@hasna/loops 0.4.13 → 0.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -3
- package/README.md +171 -39
- package/dist/api/index.d.ts +36 -0
- package/dist/api/index.js +1518 -15
- package/dist/cli/index.js +7280 -3213
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +1433 -303
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8689 -4837
- package/dist/lib/cloud/mode.d.ts +17 -0
- package/dist/lib/cloud/resolve.d.ts +16 -0
- package/dist/lib/cloud/storage.d.ts +82 -0
- package/dist/lib/cloud/transport.d.ts +148 -0
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +15 -3
- package/dist/lib/route/index.d.ts +2 -0
- package/dist/lib/route/policies.d.ts +51 -0
- package/dist/lib/route/provider-admission.d.ts +37 -0
- package/dist/lib/route/throttle.d.ts +4 -0
- package/dist/lib/route/types.d.ts +8 -0
- package/dist/lib/run-receipts.d.ts +14 -0
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +2028 -231
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +120 -0
- package/dist/lib/storage/postgres-schema.js +29 -0
- package/dist/lib/storage/postgres.js +29 -0
- package/dist/lib/storage/sqlite.d.ts +6 -0
- package/dist/lib/storage/sqlite.js +748 -26
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +282 -1
- package/dist/lib/store.js +746 -26
- package/dist/lib/template-kit.d.ts +25 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/index.js +2885 -634
- package/dist/runner/index.js +198 -32
- package/dist/sdk/http.d.ts +182 -0
- package/dist/sdk/http.js +166 -0
- package/dist/sdk/index.d.ts +55 -18
- package/dist/sdk/index.js +2734 -617
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +9095 -0
- package/dist/types.d.ts +52 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +78 -0
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +195 -38
- package/package.json +15 -3
package/dist/daemon/index.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// src/lib/store.ts
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
|
-
import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync as
|
|
7
|
-
import { tmpdir } from "os";
|
|
8
|
-
import { basename as basename2, dirname as dirname2, join as
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync3 } from "fs";
|
|
7
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
8
|
+
import { basename as basename2, dirname as dirname2, join as join4 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/lib/errors.ts
|
|
11
11
|
class CodedError extends Error {
|
|
@@ -1204,15 +1204,378 @@ function discardWorkflowRunManifest(staged) {
|
|
|
1204
1204
|
} catch {}
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
// src/lib/run-receipts.ts
|
|
1208
|
+
import { createHash as createHash2 } from "crypto";
|
|
1209
|
+
import { hostname } from "os";
|
|
1210
|
+
|
|
1211
|
+
// src/lib/run-envelope.ts
|
|
1212
|
+
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
1213
|
+
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
1214
|
+
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
1215
|
+
if (!text)
|
|
1216
|
+
return;
|
|
1217
|
+
const scrubbed = scrubSecrets(text);
|
|
1218
|
+
if (scrubbed.length <= limit * 2)
|
|
1219
|
+
return scrubbed;
|
|
1220
|
+
const omitted = scrubbed.length - limit * 2;
|
|
1221
|
+
return `${scrubbed.slice(0, limit)}
|
|
1222
|
+
[... ${omitted} chars omitted ...]
|
|
1223
|
+
${scrubbed.slice(-limit)}`;
|
|
1224
|
+
}
|
|
1225
|
+
function summarizeOutput(stdout, stderr) {
|
|
1226
|
+
return {
|
|
1227
|
+
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
1228
|
+
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
1229
|
+
stdoutExcerpt: boundedExcerpt(stdout),
|
|
1230
|
+
stderrExcerpt: boundedExcerpt(stderr)
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function summarizeExecutorResult(result) {
|
|
1234
|
+
return {
|
|
1235
|
+
...summarizeOutput(result.stdout, result.stderr),
|
|
1236
|
+
status: result.status,
|
|
1237
|
+
exitCode: result.exitCode,
|
|
1238
|
+
durationMs: result.durationMs,
|
|
1239
|
+
error: boundedExcerpt(result.error)
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function isBlockedStepRun(step) {
|
|
1243
|
+
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
1244
|
+
}
|
|
1245
|
+
function summarizeWorkflowStepRun(step) {
|
|
1246
|
+
return {
|
|
1247
|
+
...summarizeOutput(step.stdout, step.stderr),
|
|
1248
|
+
stepId: step.stepId,
|
|
1249
|
+
status: step.status,
|
|
1250
|
+
exitCode: step.exitCode,
|
|
1251
|
+
durationMs: step.durationMs,
|
|
1252
|
+
error: boundedExcerpt(step.error),
|
|
1253
|
+
blocked: isBlockedStepRun(step)
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function workflowRunEnvelope(run, steps) {
|
|
1257
|
+
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/lib/run-receipts.ts
|
|
1261
|
+
var RUN_RECEIPT_SUMMARY_TEXT_CHARS = 4096;
|
|
1262
|
+
var RUN_RECEIPT_MAX_IDS = 100;
|
|
1263
|
+
var RUN_RECEIPT_MAX_EVIDENCE_PATHS = 100;
|
|
1264
|
+
var RUN_RECEIPT_MAX_PATH_CHARS = 1024;
|
|
1265
|
+
function canonicalJson(value) {
|
|
1266
|
+
if (Array.isArray(value))
|
|
1267
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1268
|
+
if (value && typeof value === "object") {
|
|
1269
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
1270
|
+
}
|
|
1271
|
+
return JSON.stringify(value);
|
|
1272
|
+
}
|
|
1273
|
+
function digestReceipt(receipt) {
|
|
1274
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(receipt)).digest("hex")}`;
|
|
1275
|
+
}
|
|
1276
|
+
function isoOrNull(value, label) {
|
|
1277
|
+
if (value === undefined || value === null || value === "")
|
|
1278
|
+
return null;
|
|
1279
|
+
const date = new Date(value);
|
|
1280
|
+
if (Number.isNaN(date.getTime()))
|
|
1281
|
+
throw new Error(`${label} must be a valid ISO date/time`);
|
|
1282
|
+
return date.toISOString();
|
|
1283
|
+
}
|
|
1284
|
+
function nonEmpty(value, label) {
|
|
1285
|
+
const trimmed = value?.trim();
|
|
1286
|
+
if (!trimmed)
|
|
1287
|
+
throw new Error(`${label} must be non-empty`);
|
|
1288
|
+
return trimmed;
|
|
1289
|
+
}
|
|
1290
|
+
function normalizedStringArray(values, opts) {
|
|
1291
|
+
const out = [];
|
|
1292
|
+
const seen = new Set;
|
|
1293
|
+
for (const raw of values ?? []) {
|
|
1294
|
+
const value = String(raw).trim();
|
|
1295
|
+
if (!value || seen.has(value))
|
|
1296
|
+
continue;
|
|
1297
|
+
seen.add(value);
|
|
1298
|
+
out.push(opts.itemMax ? value.slice(0, opts.itemMax) : value);
|
|
1299
|
+
if (out.length >= opts.max)
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
if ((values?.length ?? 0) > opts.max) {
|
|
1303
|
+
out.push(`[truncated ${values.length - opts.max} ${opts.label}]`);
|
|
1304
|
+
}
|
|
1305
|
+
return out;
|
|
1306
|
+
}
|
|
1307
|
+
function receiptMachine(input, opts) {
|
|
1308
|
+
if (typeof input.machine === "string")
|
|
1309
|
+
return nonEmpty(input.machine, "machine");
|
|
1310
|
+
if (input.machine && typeof input.machine === "object")
|
|
1311
|
+
return input.machine;
|
|
1312
|
+
if (opts.loop?.machine)
|
|
1313
|
+
return { ...opts.loop.machine };
|
|
1314
|
+
if (typeof opts.defaultMachine === "string")
|
|
1315
|
+
return nonEmpty(opts.defaultMachine, "machine");
|
|
1316
|
+
if (opts.defaultMachine && typeof opts.defaultMachine === "object")
|
|
1317
|
+
return opts.defaultMachine;
|
|
1318
|
+
return hostname();
|
|
1319
|
+
}
|
|
1320
|
+
function normalizeSummary(input, run) {
|
|
1321
|
+
const stdout = input.stdout ?? run?.stdout;
|
|
1322
|
+
const stderr = input.stderr ?? run?.stderr;
|
|
1323
|
+
const output = summarizeOutput(stdout, stderr);
|
|
1324
|
+
const summaryInput = input.summary;
|
|
1325
|
+
const provided = typeof summaryInput === "object" && summaryInput !== null ? summaryInput : {};
|
|
1326
|
+
const text = typeof summaryInput === "string" ? boundedExcerpt(summaryInput, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : typeof provided.text === "string" ? boundedExcerpt(provided.text, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : undefined;
|
|
1327
|
+
return {
|
|
1328
|
+
text,
|
|
1329
|
+
stdout_bytes: Number.isFinite(provided.stdout_bytes) ? Number(provided.stdout_bytes) : output.stdoutBytes,
|
|
1330
|
+
stderr_bytes: Number.isFinite(provided.stderr_bytes) ? Number(provided.stderr_bytes) : output.stderrBytes,
|
|
1331
|
+
stdout_excerpt: typeof provided.stdout_excerpt === "string" ? boundedExcerpt(provided.stdout_excerpt) : output.stdoutExcerpt,
|
|
1332
|
+
stderr_excerpt: typeof provided.stderr_excerpt === "string" ? boundedExcerpt(provided.stderr_excerpt) : output.stderrExcerpt,
|
|
1333
|
+
error: typeof provided.error === "string" ? boundedExcerpt(provided.error) : boundedExcerpt(input.error ?? run?.error),
|
|
1334
|
+
duration_ms: Number.isFinite(provided.duration_ms) ? Number(provided.duration_ms) : input.duration_ms ?? run?.durationMs
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function normalizeRunReceipt(input, opts = {}) {
|
|
1338
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1339
|
+
const run = opts.run;
|
|
1340
|
+
const loopId = input.loop_id ?? run?.loopId ?? opts.loop?.id;
|
|
1341
|
+
const normalized = {
|
|
1342
|
+
loop_id: nonEmpty(loopId, "loop_id"),
|
|
1343
|
+
run_id: nonEmpty(input.run_id ?? run?.id, "run_id"),
|
|
1344
|
+
machine: receiptMachine(input, opts),
|
|
1345
|
+
repo: nonEmpty(input.repo ?? opts.defaultRepo ?? targetRepo(opts.loop) ?? process.cwd(), "repo"),
|
|
1346
|
+
task_ids: normalizedStringArray(input.task_ids, { max: RUN_RECEIPT_MAX_IDS, label: "task_ids" }),
|
|
1347
|
+
knowledge_ids: normalizedStringArray(input.knowledge_ids, { max: RUN_RECEIPT_MAX_IDS, label: "knowledge_ids" }),
|
|
1348
|
+
started_at: isoOrNull(input.started_at ?? run?.startedAt, "started_at"),
|
|
1349
|
+
finished_at: isoOrNull(input.finished_at ?? run?.finishedAt, "finished_at"),
|
|
1350
|
+
status: nonEmpty(input.status ?? run?.status, "status"),
|
|
1351
|
+
exit_code: input.exit_code === undefined ? run?.exitCode ?? null : input.exit_code,
|
|
1352
|
+
summary: normalizeSummary(input, run),
|
|
1353
|
+
evidence_paths: normalizedStringArray(input.evidence_paths, {
|
|
1354
|
+
max: RUN_RECEIPT_MAX_EVIDENCE_PATHS,
|
|
1355
|
+
label: "evidence_paths",
|
|
1356
|
+
itemMax: RUN_RECEIPT_MAX_PATH_CHARS
|
|
1357
|
+
})
|
|
1358
|
+
};
|
|
1359
|
+
return {
|
|
1360
|
+
...normalized,
|
|
1361
|
+
digest_id: input.digest_id?.trim() || digestReceipt(normalized),
|
|
1362
|
+
created_at: opts.existing?.created_at ?? now,
|
|
1363
|
+
updated_at: now
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function targetRepo(loop) {
|
|
1367
|
+
if (!loop)
|
|
1368
|
+
return;
|
|
1369
|
+
if (loop.target.type === "command")
|
|
1370
|
+
return loop.target.cwd;
|
|
1371
|
+
if (loop.target.type === "agent")
|
|
1372
|
+
return loop.target.cwd;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/lib/route/todos-cli.ts
|
|
1377
|
+
import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
1378
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1379
|
+
import { join as join3 } from "path";
|
|
1380
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
1381
|
+
|
|
1382
|
+
// src/lib/format.ts
|
|
1383
|
+
var TEXT_OUTPUT_LIMIT = 32 * 1024;
|
|
1384
|
+
var SENSITIVE_PAYLOAD_KEYS = new Set(["env", "error", "prompt", "reason", "stderr", "stdout"]);
|
|
1385
|
+
function redact(value, visible = 0) {
|
|
1386
|
+
if (!value)
|
|
1387
|
+
return value;
|
|
1388
|
+
if (value.length <= visible)
|
|
1389
|
+
return value;
|
|
1390
|
+
if (visible <= 0)
|
|
1391
|
+
return `[redacted ${value.length} chars]`;
|
|
1392
|
+
return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
|
|
1393
|
+
}
|
|
1394
|
+
function truncateTextOutput(value) {
|
|
1395
|
+
if (value.length <= TEXT_OUTPUT_LIMIT)
|
|
1396
|
+
return value;
|
|
1397
|
+
return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
|
|
1398
|
+
[truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
|
|
1399
|
+
}
|
|
1400
|
+
function redactSensitivePayload(value, key) {
|
|
1401
|
+
if (key && SENSITIVE_PAYLOAD_KEYS.has(key)) {
|
|
1402
|
+
if (typeof value === "string")
|
|
1403
|
+
return redact(value);
|
|
1404
|
+
if (value === undefined || value === null)
|
|
1405
|
+
return value;
|
|
1406
|
+
return "[redacted]";
|
|
1407
|
+
}
|
|
1408
|
+
if (Array.isArray(value))
|
|
1409
|
+
return value.map((item) => redactSensitivePayload(item));
|
|
1410
|
+
if (value && typeof value === "object") {
|
|
1411
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactSensitivePayload(entryValue, entryKey)]));
|
|
1412
|
+
}
|
|
1413
|
+
return value;
|
|
1414
|
+
}
|
|
1415
|
+
function textOutputBlocks(value, opts = {}) {
|
|
1416
|
+
const indent = opts.indent ?? "";
|
|
1417
|
+
const nested = `${indent} `;
|
|
1418
|
+
const blocks = [];
|
|
1419
|
+
for (const [label, output] of [
|
|
1420
|
+
["stdout", value.stdout],
|
|
1421
|
+
["stderr", value.stderr]
|
|
1422
|
+
]) {
|
|
1423
|
+
if (!output)
|
|
1424
|
+
continue;
|
|
1425
|
+
blocks.push(`${indent}${label}:`);
|
|
1426
|
+
for (const line of truncateTextOutput(output).replace(/\s+$/, "").split(/\r?\n/)) {
|
|
1427
|
+
blocks.push(`${nested}${line}`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return blocks;
|
|
1431
|
+
}
|
|
1432
|
+
function publicLoop(loop) {
|
|
1433
|
+
const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : loop.target.type === "agent" ? { ...loop.target, prompt: redact(loop.target.prompt) } : loop.target;
|
|
1434
|
+
return {
|
|
1435
|
+
...loop,
|
|
1436
|
+
target
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function publicRun(run, showOutput = false, opts = {}) {
|
|
1440
|
+
return {
|
|
1441
|
+
...run,
|
|
1442
|
+
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
1443
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
1444
|
+
error: opts.redactError ? redact(run.error) : run.error
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
function publicRunReceipt(receipt) {
|
|
1448
|
+
return { ...receipt };
|
|
1449
|
+
}
|
|
1450
|
+
function publicExecutorResult(result, showOutput = false) {
|
|
1451
|
+
return {
|
|
1452
|
+
...result,
|
|
1453
|
+
stdout: showOutput ? result.stdout : result.stdout ? `[redacted ${result.stdout.length} chars]` : undefined,
|
|
1454
|
+
stderr: showOutput ? result.stderr : result.stderr ? `[redacted ${result.stderr.length} chars]` : undefined,
|
|
1455
|
+
error: redact(result.error)
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function publicWorkflow(workflow) {
|
|
1459
|
+
return {
|
|
1460
|
+
...workflow,
|
|
1461
|
+
steps: workflow.steps.map((step) => ({
|
|
1462
|
+
...step,
|
|
1463
|
+
target: step.target.type === "agent" ? { ...step.target, prompt: redact(step.target.prompt) } : step.target.type === "command" && step.target.env ? { ...step.target, env: "[redacted]" } : step.target
|
|
1464
|
+
}))
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function publicWorkflowRun(run) {
|
|
1468
|
+
return { ...run, error: redact(run.error) };
|
|
1469
|
+
}
|
|
1470
|
+
function publicWorkflowInvocation(invocation) {
|
|
1471
|
+
return redactSensitivePayload(invocation);
|
|
1472
|
+
}
|
|
1473
|
+
function publicWorkflowWorkItem(item) {
|
|
1474
|
+
return { ...item, lastReason: redact(item.lastReason, 240) };
|
|
1475
|
+
}
|
|
1476
|
+
function publicWorkflowStepRun(run, showOutput = false) {
|
|
1477
|
+
return {
|
|
1478
|
+
...run,
|
|
1479
|
+
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
1480
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
1481
|
+
error: redact(run.error)
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function publicWorkflowEvent(event) {
|
|
1485
|
+
return { ...event, payload: redactSensitivePayload(event.payload) };
|
|
1486
|
+
}
|
|
1487
|
+
function publicGoal(goal) {
|
|
1488
|
+
return {
|
|
1489
|
+
...goal,
|
|
1490
|
+
objective: redact(goal.objective, 120)
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function publicGoalRun(run) {
|
|
1494
|
+
return {
|
|
1495
|
+
...run,
|
|
1496
|
+
evidence: redactSensitivePayload(run.evidence),
|
|
1497
|
+
rawResponse: redactSensitivePayload(run.rawResponse)
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/lib/route/todos-cli.ts
|
|
1502
|
+
function defaultLoopsProject() {
|
|
1503
|
+
return process.env.LOOPS_TASK_PROJECT || process.env.LOOPS_DATA_DIR || `${process.env.HOME || homedir2()}/.hasna/loops`;
|
|
1504
|
+
}
|
|
1505
|
+
function runLocalCommand(command, args, opts = {}) {
|
|
1506
|
+
const result = spawnSync2(command, args, {
|
|
1507
|
+
input: opts.input,
|
|
1508
|
+
encoding: "utf8",
|
|
1509
|
+
timeout: opts.timeoutMs ?? 30000,
|
|
1510
|
+
maxBuffer: opts.maxBuffer ?? 8 * 1024 * 1024,
|
|
1511
|
+
env: process.env
|
|
1512
|
+
});
|
|
1513
|
+
return {
|
|
1514
|
+
ok: result.status === 0,
|
|
1515
|
+
status: result.status,
|
|
1516
|
+
stdout: result.stdout || "",
|
|
1517
|
+
stderr: result.stderr || "",
|
|
1518
|
+
error: result.error ? String(result.error.message || result.error) : ""
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
1522
|
+
const tempDir = mkdtempSync(join3(tmpdir(), "loops-command-output-"));
|
|
1523
|
+
const stdoutPath = join3(tempDir, "stdout");
|
|
1524
|
+
const stdoutFd = openSync(stdoutPath, "w");
|
|
1525
|
+
let result;
|
|
1526
|
+
try {
|
|
1527
|
+
result = spawnSync2(command, args, {
|
|
1528
|
+
input: opts.input,
|
|
1529
|
+
encoding: "utf8",
|
|
1530
|
+
timeout: opts.timeoutMs ?? 30000,
|
|
1531
|
+
maxBuffer: opts.maxBuffer ?? 8 * 1024 * 1024,
|
|
1532
|
+
env: process.env,
|
|
1533
|
+
stdio: ["pipe", stdoutFd, "pipe"]
|
|
1534
|
+
});
|
|
1535
|
+
} finally {
|
|
1536
|
+
closeSync(stdoutFd);
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
return {
|
|
1540
|
+
ok: result.status === 0,
|
|
1541
|
+
status: result.status,
|
|
1542
|
+
stdout: readFileSync3(stdoutPath, "utf8"),
|
|
1543
|
+
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
|
|
1544
|
+
error: result.error ? String(result.error.message || result.error) : ""
|
|
1545
|
+
};
|
|
1546
|
+
} finally {
|
|
1547
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function ensureTodosTaskList(project, slug, name, description) {
|
|
1551
|
+
runLocalCommand("todos", ["--project", project, "task-lists", "--add", name, "--slug", slug, "-d", description]);
|
|
1552
|
+
const list = runLocalCommand("todos", ["--project", project, "--json", "task-lists"]);
|
|
1553
|
+
if (!list.ok)
|
|
1554
|
+
throw new Error(list.stderr || list.error || "failed to list todos task lists");
|
|
1555
|
+
const values = JSON.parse(list.stdout || "[]");
|
|
1556
|
+
const found = values.find((entry) => entry.slug === slug);
|
|
1557
|
+
if (!found)
|
|
1558
|
+
throw new Error(`todos task list not found after ensure: ${slug}`);
|
|
1559
|
+
return found.id;
|
|
1560
|
+
}
|
|
1561
|
+
function todosMutationSummary(result) {
|
|
1562
|
+
return {
|
|
1563
|
+
ok: result.ok,
|
|
1564
|
+
status: result.status,
|
|
1565
|
+
error: result.ok ? undefined : redact(result.stderr || result.error || "todos mutation failed", 320)
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1207
1569
|
// src/lib/store.ts
|
|
1208
1570
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
1209
1571
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
1210
1572
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
1211
|
-
var SCHEMA_USER_VERSION =
|
|
1573
|
+
var SCHEMA_USER_VERSION = 8;
|
|
1212
1574
|
var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
1213
1575
|
var PRUNE_BATCH_SIZE = 400;
|
|
1214
1576
|
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
1215
1577
|
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
1578
|
+
var TASK_LIFECYCLE_TEMPLATE_ID = "task-lifecycle";
|
|
1216
1579
|
function rowToLoop(row) {
|
|
1217
1580
|
return {
|
|
1218
1581
|
id: row.id,
|
|
@@ -1263,6 +1626,28 @@ function rowToRun(row) {
|
|
|
1263
1626
|
updatedAt: row.updated_at
|
|
1264
1627
|
};
|
|
1265
1628
|
}
|
|
1629
|
+
function rowToRunReceipt(row) {
|
|
1630
|
+
return {
|
|
1631
|
+
loop_id: row.loop_id,
|
|
1632
|
+
run_id: row.run_id,
|
|
1633
|
+
machine: JSON.parse(row.machine_json),
|
|
1634
|
+
repo: row.repo,
|
|
1635
|
+
task_ids: JSON.parse(row.task_ids_json),
|
|
1636
|
+
knowledge_ids: JSON.parse(row.knowledge_ids_json),
|
|
1637
|
+
digest_id: row.digest_id,
|
|
1638
|
+
started_at: row.started_at,
|
|
1639
|
+
finished_at: row.finished_at,
|
|
1640
|
+
status: row.status,
|
|
1641
|
+
exit_code: row.exit_code,
|
|
1642
|
+
summary: JSON.parse(row.summary_json),
|
|
1643
|
+
evidence_paths: JSON.parse(row.evidence_paths_json),
|
|
1644
|
+
created_at: row.created_at,
|
|
1645
|
+
updated_at: row.updated_at
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function latestRunTime(row) {
|
|
1649
|
+
return row.finished_at ?? row.started_at ?? row.created_at;
|
|
1650
|
+
}
|
|
1266
1651
|
function rowToWorkflow(row) {
|
|
1267
1652
|
return {
|
|
1268
1653
|
id: row.id,
|
|
@@ -1323,6 +1708,7 @@ function rowToWorkflowWorkItem(row) {
|
|
|
1323
1708
|
subjectRef: row.subject_ref,
|
|
1324
1709
|
projectKey: row.project_key ?? undefined,
|
|
1325
1710
|
projectGroup: row.project_group ?? undefined,
|
|
1711
|
+
machineId: row.machine_id ?? undefined,
|
|
1326
1712
|
routeScope: row.route_scope ?? undefined,
|
|
1327
1713
|
priority: row.priority,
|
|
1328
1714
|
status: row.status,
|
|
@@ -1480,6 +1866,7 @@ function scrubbedOrNull(value) {
|
|
|
1480
1866
|
return value == null ? null : scrubSecrets(value);
|
|
1481
1867
|
}
|
|
1482
1868
|
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1869
|
+
var MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS = 64 * 1024;
|
|
1483
1870
|
function clampPersistedRunOutput(value) {
|
|
1484
1871
|
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1485
1872
|
return value;
|
|
@@ -1494,6 +1881,42 @@ ${tail}`;
|
|
|
1494
1881
|
function persistedRunOutput(value) {
|
|
1495
1882
|
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1496
1883
|
}
|
|
1884
|
+
function clampTextToChars(value, maxChars, reason) {
|
|
1885
|
+
if (value.length <= maxChars)
|
|
1886
|
+
return value;
|
|
1887
|
+
const marker = `
|
|
1888
|
+
...[truncated by ${reason}]...
|
|
1889
|
+
`;
|
|
1890
|
+
const budget = Math.max(0, maxChars - marker.length);
|
|
1891
|
+
const headLength = Math.ceil(budget / 2);
|
|
1892
|
+
const tailLength = Math.floor(budget / 2);
|
|
1893
|
+
return `${value.slice(0, headLength)}${marker}${value.slice(value.length - tailLength)}`;
|
|
1894
|
+
}
|
|
1895
|
+
function boundedWorkflowEventPayloadJson(scrubbedJson) {
|
|
1896
|
+
if (scrubbedJson.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS)
|
|
1897
|
+
return scrubbedJson;
|
|
1898
|
+
const base = {
|
|
1899
|
+
truncated: true,
|
|
1900
|
+
originalChars: scrubbedJson.length,
|
|
1901
|
+
maxChars: MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS,
|
|
1902
|
+
preview: ""
|
|
1903
|
+
};
|
|
1904
|
+
const baseChars = JSON.stringify(base).length;
|
|
1905
|
+
let previewBudget = Math.max(0, MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS - baseChars - 64);
|
|
1906
|
+
while (true) {
|
|
1907
|
+
const preview = clampTextToChars(scrubbedJson, previewBudget, "loops workflow-event payload retention");
|
|
1908
|
+
const bounded = JSON.stringify({ ...base, preview });
|
|
1909
|
+
if (bounded.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS || previewBudget === 0)
|
|
1910
|
+
return bounded;
|
|
1911
|
+
previewBudget = Math.max(0, previewBudget - (bounded.length - MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS) - 64);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function persistedWorkflowEventPayload(payload) {
|
|
1915
|
+
if (payload == null)
|
|
1916
|
+
return null;
|
|
1917
|
+
const scrubbed = scrubSecretsDeep(payload);
|
|
1918
|
+
return boundedWorkflowEventPayloadJson(scrubSecrets(JSON.stringify(scrubbed)));
|
|
1919
|
+
}
|
|
1497
1920
|
function chmodIfExists(path, mode) {
|
|
1498
1921
|
try {
|
|
1499
1922
|
if (existsSync(path))
|
|
@@ -1517,7 +1940,7 @@ class Store {
|
|
|
1517
1940
|
const file = path ?? dbPath();
|
|
1518
1941
|
if (file !== ":memory:")
|
|
1519
1942
|
ensurePrivateStorePath(file);
|
|
1520
|
-
this.rootDir = file === ":memory:" ?
|
|
1943
|
+
this.rootDir = file === ":memory:" ? mkdtempSync2(join4(tmpdir2(), "open-loops-store-")) : dirname2(file);
|
|
1521
1944
|
if (file === ":memory:")
|
|
1522
1945
|
this.memoryRootDir = this.rootDir;
|
|
1523
1946
|
this.db = new Database(file);
|
|
@@ -1611,6 +2034,17 @@ class Store {
|
|
|
1611
2034
|
this.addColumnIfMissing("workflow_work_items", "route_scope", "TEXT");
|
|
1612
2035
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status)");
|
|
1613
2036
|
}
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
id: "0009_run_receipts",
|
|
2040
|
+
apply: () => this.createRunReceiptsSchema()
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
id: "0010_work_item_machine_id",
|
|
2044
|
+
apply: () => {
|
|
2045
|
+
this.addColumnIfMissing("workflow_work_items", "machine_id", "TEXT");
|
|
2046
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status)");
|
|
2047
|
+
}
|
|
1614
2048
|
}
|
|
1615
2049
|
];
|
|
1616
2050
|
}
|
|
@@ -1672,6 +2106,28 @@ class Store {
|
|
|
1672
2106
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1673
2107
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1674
2108
|
|
|
2109
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2110
|
+
run_id TEXT PRIMARY KEY,
|
|
2111
|
+
loop_id TEXT NOT NULL,
|
|
2112
|
+
machine_json TEXT NOT NULL,
|
|
2113
|
+
repo TEXT NOT NULL,
|
|
2114
|
+
task_ids_json TEXT NOT NULL,
|
|
2115
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2116
|
+
digest_id TEXT NOT NULL,
|
|
2117
|
+
started_at TEXT,
|
|
2118
|
+
finished_at TEXT,
|
|
2119
|
+
status TEXT NOT NULL,
|
|
2120
|
+
exit_code INTEGER,
|
|
2121
|
+
summary_json TEXT NOT NULL,
|
|
2122
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2123
|
+
created_at TEXT NOT NULL,
|
|
2124
|
+
updated_at TEXT NOT NULL
|
|
2125
|
+
);
|
|
2126
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2127
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2128
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2129
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2130
|
+
|
|
1675
2131
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1676
2132
|
id TEXT PRIMARY KEY,
|
|
1677
2133
|
pid INTEGER NOT NULL,
|
|
@@ -1758,6 +2214,7 @@ class Store {
|
|
|
1758
2214
|
subject_ref TEXT NOT NULL,
|
|
1759
2215
|
project_key TEXT,
|
|
1760
2216
|
project_group TEXT,
|
|
2217
|
+
machine_id TEXT,
|
|
1761
2218
|
route_scope TEXT,
|
|
1762
2219
|
priority INTEGER NOT NULL,
|
|
1763
2220
|
status TEXT NOT NULL,
|
|
@@ -1776,10 +2233,10 @@ class Store {
|
|
|
1776
2233
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
1777
2234
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
1778
2235
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
1779
|
-
--
|
|
1780
|
-
--
|
|
2236
|
+
-- New-column indexes (route_scope, machine_id, etc.) are created ONLY by
|
|
2237
|
+
-- their additive migrations, never here: this baseline DDL
|
|
1781
2238
|
-- re-runs on EVERY open (0001 is not skip-guarded), and on a pre-0008
|
|
1782
|
-
-- database the CREATE TABLE above is a no-op, so an index on
|
|
2239
|
+
-- database the CREATE TABLE above is a no-op, so an index on a new column
|
|
1783
2240
|
-- here would execute before the column exists and crash the open
|
|
1784
2241
|
-- ("no such column: route_scope"). New columns may be folded into the
|
|
1785
2242
|
-- CREATE TABLE (fresh-db only); their indexes must live in the migration.
|
|
@@ -1890,6 +2347,31 @@ class Store {
|
|
|
1890
2347
|
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
1891
2348
|
`);
|
|
1892
2349
|
}
|
|
2350
|
+
createRunReceiptsSchema() {
|
|
2351
|
+
this.db.exec(`
|
|
2352
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2353
|
+
run_id TEXT PRIMARY KEY,
|
|
2354
|
+
loop_id TEXT NOT NULL,
|
|
2355
|
+
machine_json TEXT NOT NULL,
|
|
2356
|
+
repo TEXT NOT NULL,
|
|
2357
|
+
task_ids_json TEXT NOT NULL,
|
|
2358
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2359
|
+
digest_id TEXT NOT NULL,
|
|
2360
|
+
started_at TEXT,
|
|
2361
|
+
finished_at TEXT,
|
|
2362
|
+
status TEXT NOT NULL,
|
|
2363
|
+
exit_code INTEGER,
|
|
2364
|
+
summary_json TEXT NOT NULL,
|
|
2365
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2366
|
+
created_at TEXT NOT NULL,
|
|
2367
|
+
updated_at TEXT NOT NULL
|
|
2368
|
+
);
|
|
2369
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2370
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2371
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2372
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2373
|
+
`);
|
|
2374
|
+
}
|
|
1893
2375
|
addColumnIfMissing(table, column, definition) {
|
|
1894
2376
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
1895
2377
|
if (columns.some((c) => c.name === column))
|
|
@@ -2001,21 +2483,51 @@ class Store {
|
|
|
2001
2483
|
}
|
|
2002
2484
|
listLoops(opts = {}) {
|
|
2003
2485
|
const limit = opts.limit ?? 200;
|
|
2486
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
2487
|
+
if (opts.name != null) {
|
|
2488
|
+
const rows2 = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.name, limit, offset);
|
|
2489
|
+
return this.withLatestRunSummaries(rows2.map(rowToLoop));
|
|
2490
|
+
}
|
|
2004
2491
|
let rows;
|
|
2005
2492
|
if (opts.status && opts.archived) {
|
|
2006
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2493
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2007
2494
|
} else if (opts.status && opts.includeArchived) {
|
|
2008
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2495
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2009
2496
|
} else if (opts.status) {
|
|
2010
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2497
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2011
2498
|
} else if (opts.archived) {
|
|
2012
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
2499
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2013
2500
|
} else if (opts.includeArchived) {
|
|
2014
|
-
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2501
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2015
2502
|
} else {
|
|
2016
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2017
|
-
}
|
|
2018
|
-
return rows.map(rowToLoop);
|
|
2503
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2504
|
+
}
|
|
2505
|
+
return this.withLatestRunSummaries(rows.map(rowToLoop));
|
|
2506
|
+
}
|
|
2507
|
+
withLatestRunSummaries(loops) {
|
|
2508
|
+
if (loops.length === 0)
|
|
2509
|
+
return loops;
|
|
2510
|
+
const placeholders = loops.map(() => "?").join(",");
|
|
2511
|
+
const rows = this.db.query(`SELECT loop_id, id, status, started_at, finished_at, created_at
|
|
2512
|
+
FROM (
|
|
2513
|
+
SELECT loop_id, id, status, started_at, finished_at, created_at,
|
|
2514
|
+
ROW_NUMBER() OVER (PARTITION BY loop_id ORDER BY created_at DESC, id DESC) AS rn
|
|
2515
|
+
FROM loop_runs
|
|
2516
|
+
WHERE loop_id IN (${placeholders})
|
|
2517
|
+
)
|
|
2518
|
+
WHERE rn = 1`).all(...loops.map((loop) => loop.id));
|
|
2519
|
+
const latestByLoopId = new Map(rows.map((row) => [row.loop_id, row]));
|
|
2520
|
+
return loops.map((loop) => {
|
|
2521
|
+
const latest = latestByLoopId.get(loop.id);
|
|
2522
|
+
if (!latest)
|
|
2523
|
+
return loop;
|
|
2524
|
+
return {
|
|
2525
|
+
...loop,
|
|
2526
|
+
latestRunId: latest.id,
|
|
2527
|
+
latestRunStatus: latest.status,
|
|
2528
|
+
lastRunAt: latestRunTime(latest)
|
|
2529
|
+
};
|
|
2530
|
+
});
|
|
2019
2531
|
}
|
|
2020
2532
|
dueLoops(now, limit = 500) {
|
|
2021
2533
|
const rows = this.db.query(`SELECT * FROM loops
|
|
@@ -2134,6 +2646,45 @@ class Store {
|
|
|
2134
2646
|
throw error;
|
|
2135
2647
|
}
|
|
2136
2648
|
}
|
|
2649
|
+
updateAgentLoopTimeout(idOrName, timeoutMs, opts = {}) {
|
|
2650
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
2651
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2652
|
+
try {
|
|
2653
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
2654
|
+
if (current.archivedAt)
|
|
2655
|
+
throw new LoopArchivedError(current.name || current.id);
|
|
2656
|
+
if (current.target.type !== "agent")
|
|
2657
|
+
throw new Error(`loop is not an agent loop: ${idOrName}`);
|
|
2658
|
+
if (this.hasRunningRun(current.id))
|
|
2659
|
+
throw new Error(`refusing to update running loop: ${current.id}`);
|
|
2660
|
+
const target = { ...current.target, timeoutMs };
|
|
2661
|
+
if (timeoutMs === null && target.idleTimeoutMs !== undefined)
|
|
2662
|
+
delete target.idleTimeoutMs;
|
|
2663
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
2664
|
+
WHERE id=$id
|
|
2665
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
2666
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
2667
|
+
))`).run({
|
|
2668
|
+
$id: current.id,
|
|
2669
|
+
$target: JSON.stringify(target),
|
|
2670
|
+
$updated: updated,
|
|
2671
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
2672
|
+
$now: updated
|
|
2673
|
+
});
|
|
2674
|
+
if (res.changes !== 1)
|
|
2675
|
+
throw new Error("daemon lease lost");
|
|
2676
|
+
this.db.exec("COMMIT");
|
|
2677
|
+
const after = this.getLoop(current.id);
|
|
2678
|
+
if (!after)
|
|
2679
|
+
throw new Error(`loop not found after timeout update: ${current.id}`);
|
|
2680
|
+
return after;
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
try {
|
|
2683
|
+
this.db.exec("ROLLBACK");
|
|
2684
|
+
} catch {}
|
|
2685
|
+
throw error;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2137
2688
|
createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
|
|
2138
2689
|
const normalized = normalizeCreateWorkflowInput(workflowInput);
|
|
2139
2690
|
const updated = (opts.now ?? new Date).toISOString();
|
|
@@ -2464,6 +3015,59 @@ class Store {
|
|
|
2464
3015
|
updated
|
|
2465
3016
|
});
|
|
2466
3017
|
}
|
|
3018
|
+
taskLifecycleTodosPointerContext(workflowRunId) {
|
|
3019
|
+
const run = this.getWorkflowRun(workflowRunId);
|
|
3020
|
+
if (!run || run.status !== "succeeded" || !run.invocationId || !run.workItemId || !run.manifestPath)
|
|
3021
|
+
return;
|
|
3022
|
+
const workItem = this.getWorkflowWorkItem(run.workItemId);
|
|
3023
|
+
if (!workItem || workItem.routeKey !== "todos-task")
|
|
3024
|
+
return;
|
|
3025
|
+
const invocation = this.getWorkflowInvocation(run.invocationId);
|
|
3026
|
+
if (!invocation || invocation.templateId !== TASK_LIFECYCLE_TEMPLATE_ID)
|
|
3027
|
+
return;
|
|
3028
|
+
const projectPath = workItem.projectKey ?? invocation.scope?.projectPath;
|
|
3029
|
+
const taskId = invocation.subjectRef.id ?? workItem.subjectRef;
|
|
3030
|
+
if (!projectPath || !taskId)
|
|
3031
|
+
return;
|
|
3032
|
+
return {
|
|
3033
|
+
projectPath,
|
|
3034
|
+
taskId,
|
|
3035
|
+
invocationId: invocation.id,
|
|
3036
|
+
workflowRunId: run.id,
|
|
3037
|
+
manifestPath: run.manifestPath
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
syncSuccessfulTaskLifecycleTodosPointers(workflowRunId) {
|
|
3041
|
+
const context = this.taskLifecycleTodosPointerContext(workflowRunId);
|
|
3042
|
+
if (!context)
|
|
3043
|
+
return;
|
|
3044
|
+
const result = runLocalCommand("todos", [
|
|
3045
|
+
"--project",
|
|
3046
|
+
context.projectPath,
|
|
3047
|
+
"task",
|
|
3048
|
+
"workflow-pointers",
|
|
3049
|
+
context.taskId,
|
|
3050
|
+
"--clear",
|
|
3051
|
+
"--invocation",
|
|
3052
|
+
context.invocationId,
|
|
3053
|
+
"--run",
|
|
3054
|
+
context.workflowRunId,
|
|
3055
|
+
"--manifest",
|
|
3056
|
+
context.manifestPath,
|
|
3057
|
+
"--state",
|
|
3058
|
+
"succeeded",
|
|
3059
|
+
"--actor",
|
|
3060
|
+
"openloops:task-lifecycle"
|
|
3061
|
+
]);
|
|
3062
|
+
this.appendWorkflowEvent(workflowRunId, result.ok ? "todos_workflow_pointers_synced" : "todos_workflow_pointers_sync_failed", undefined, {
|
|
3063
|
+
projectPath: context.projectPath,
|
|
3064
|
+
taskId: context.taskId,
|
|
3065
|
+
invocationId: context.invocationId,
|
|
3066
|
+
workflowRunId: context.workflowRunId,
|
|
3067
|
+
manifestPath: context.manifestPath,
|
|
3068
|
+
mutation: todosMutationSummary(result)
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
2467
3071
|
createWorkflowInvocation(input) {
|
|
2468
3072
|
const now = nowIso();
|
|
2469
3073
|
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
@@ -2574,10 +3178,10 @@ class Store {
|
|
|
2574
3178
|
const id = genId();
|
|
2575
3179
|
const status = input.status ?? "queued";
|
|
2576
3180
|
this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
|
|
2577
|
-
subject_ref, project_key, project_group, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
3181
|
+
subject_ref, project_key, project_group, machine_id, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
2578
3182
|
workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
|
|
2579
3183
|
VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
|
|
2580
|
-
$projectKey, $projectGroup, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
3184
|
+
$projectKey, $projectGroup, $machineId, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
2581
3185
|
$lastReason, $created, $updated)
|
|
2582
3186
|
ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
|
|
2583
3187
|
invocation_id=excluded.invocation_id,
|
|
@@ -2586,6 +3190,10 @@ class Store {
|
|
|
2586
3190
|
subject_ref=excluded.subject_ref,
|
|
2587
3191
|
project_key=excluded.project_key,
|
|
2588
3192
|
project_group=excluded.project_group,
|
|
3193
|
+
machine_id=CASE
|
|
3194
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.machine_id
|
|
3195
|
+
ELSE excluded.machine_id
|
|
3196
|
+
END,
|
|
2589
3197
|
route_scope=excluded.route_scope,
|
|
2590
3198
|
priority=excluded.priority,
|
|
2591
3199
|
status=CASE
|
|
@@ -2628,6 +3236,7 @@ class Store {
|
|
|
2628
3236
|
$subjectRef: input.subjectRef,
|
|
2629
3237
|
$projectKey: input.projectKey ?? null,
|
|
2630
3238
|
$projectGroup: input.projectGroup ?? null,
|
|
3239
|
+
$machineId: input.machineId ?? null,
|
|
2631
3240
|
$routeScope: input.routeScope ?? null,
|
|
2632
3241
|
$priority: input.priority ?? 0,
|
|
2633
3242
|
$status: status,
|
|
@@ -3137,7 +3746,7 @@ class Store {
|
|
|
3137
3746
|
VALUES ($id, $workflowRunId, 1, 'created', NULL, $payload, $created)`).run({
|
|
3138
3747
|
$id: genId(),
|
|
3139
3748
|
$workflowRunId: runId,
|
|
3140
|
-
$payload:
|
|
3749
|
+
$payload: persistedWorkflowEventPayload({
|
|
3141
3750
|
workflowId: input.workflow.id,
|
|
3142
3751
|
workflowName: input.workflow.name,
|
|
3143
3752
|
stepCount: input.workflow.steps.length,
|
|
@@ -3426,6 +4035,8 @@ class Store {
|
|
|
3426
4035
|
const run = this.getWorkflowRun(workflowRunId);
|
|
3427
4036
|
if (!run)
|
|
3428
4037
|
throw new Error(`workflow run not found after finalize: ${workflowRunId}`);
|
|
4038
|
+
if (changed && status === "succeeded")
|
|
4039
|
+
this.syncSuccessfulTaskLifecycleTodosPointers(workflowRunId);
|
|
3429
4040
|
return run;
|
|
3430
4041
|
}
|
|
3431
4042
|
cancelWorkflowRun(workflowRunId, reason = "cancelled by user") {
|
|
@@ -3466,7 +4077,7 @@ class Store {
|
|
|
3466
4077
|
$sequence: sequence,
|
|
3467
4078
|
$eventType: eventType,
|
|
3468
4079
|
$stepId: stepId ?? null,
|
|
3469
|
-
$payload:
|
|
4080
|
+
$payload: persistedWorkflowEventPayload(payload),
|
|
3470
4081
|
$created: now
|
|
3471
4082
|
});
|
|
3472
4083
|
const event = this.db.query("SELECT * FROM workflow_events WHERE id = ?").get(id);
|
|
@@ -3487,6 +4098,17 @@ class Store {
|
|
|
3487
4098
|
const row = this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE loop_id = ? AND scheduled_for = ? AND status = 'running'").get(loopId, scheduledFor);
|
|
3488
4099
|
return (row?.count ?? 0) > 0;
|
|
3489
4100
|
}
|
|
4101
|
+
hasBlockingRunningRunForOtherSlot(loopId, scheduledFor, nowIso2) {
|
|
4102
|
+
const rows = this.db.query(`SELECT * FROM loop_runs
|
|
4103
|
+
WHERE loop_id = ? AND scheduled_for <> ? AND status = 'running'`).all(loopId, scheduledFor);
|
|
4104
|
+
return rows.some((row) => {
|
|
4105
|
+
if (!row.lease_expires_at || row.lease_expires_at > nowIso2)
|
|
4106
|
+
return true;
|
|
4107
|
+
if (isRecordedProcessAlive(row.pid, row.process_started_at))
|
|
4108
|
+
return true;
|
|
4109
|
+
return this.hasLiveWorkflowStepProcesses(row.id);
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
3490
4112
|
markRunPid(id, pid, claimedBy, opts = {}) {
|
|
3491
4113
|
const now = (opts.now ?? new Date).toISOString();
|
|
3492
4114
|
const startedMs = processStartTimeMs(pid);
|
|
@@ -3619,6 +4241,10 @@ class Store {
|
|
|
3619
4241
|
loop = currentLoop;
|
|
3620
4242
|
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
3621
4243
|
const existing = this.getRunBySlot(loop.id, scheduledFor);
|
|
4244
|
+
if (loop.overlap === "skip" && this.hasBlockingRunningRunForOtherSlot(loop.id, scheduledFor, startedAt)) {
|
|
4245
|
+
this.db.exec("COMMIT");
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
3622
4248
|
if (existing) {
|
|
3623
4249
|
if (existing.status === "running") {
|
|
3624
4250
|
if (existing.leaseExpiresAt && existing.leaseExpiresAt <= startedAt && isRecordedProcessAlive(existing.pid, existing.processStartedAt)) {
|
|
@@ -3774,18 +4400,93 @@ class Store {
|
|
|
3774
4400
|
}
|
|
3775
4401
|
listRuns(opts = {}) {
|
|
3776
4402
|
const limit = opts.limit ?? 100;
|
|
4403
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
3777
4404
|
let rows;
|
|
3778
4405
|
if (opts.loopId && opts.status) {
|
|
3779
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, opts.status, limit);
|
|
4406
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, opts.status, limit, offset);
|
|
3780
4407
|
} else if (opts.loopId) {
|
|
3781
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, limit);
|
|
4408
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, limit, offset);
|
|
3782
4409
|
} else if (opts.status) {
|
|
3783
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(opts.status, limit);
|
|
4410
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
3784
4411
|
} else {
|
|
3785
|
-
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
4412
|
+
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
3786
4413
|
}
|
|
3787
4414
|
return rows.map(rowToRun);
|
|
3788
4415
|
}
|
|
4416
|
+
writeRunReceipt(input, opts = {}) {
|
|
4417
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
4418
|
+
const existing = inputRunId ? this.getRunReceipt(inputRunId) : undefined;
|
|
4419
|
+
const run = inputRunId ? this.getRun(inputRunId) : undefined;
|
|
4420
|
+
const loop = input.loop_id ? this.getLoop(input.loop_id) : run ? this.getLoop(run.loopId) : undefined;
|
|
4421
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
4422
|
+
this.db.query(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
4423
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
4424
|
+
VALUES ($runId, $loopId, $machineJson, $repo, $taskIdsJson, $knowledgeIdsJson, $digestId,
|
|
4425
|
+
$startedAt, $finishedAt, $status, $exitCode, $summaryJson, $evidencePathsJson, $createdAt, $updatedAt)
|
|
4426
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
4427
|
+
loop_id=excluded.loop_id,
|
|
4428
|
+
machine_json=excluded.machine_json,
|
|
4429
|
+
repo=excluded.repo,
|
|
4430
|
+
task_ids_json=excluded.task_ids_json,
|
|
4431
|
+
knowledge_ids_json=excluded.knowledge_ids_json,
|
|
4432
|
+
digest_id=excluded.digest_id,
|
|
4433
|
+
started_at=excluded.started_at,
|
|
4434
|
+
finished_at=excluded.finished_at,
|
|
4435
|
+
status=excluded.status,
|
|
4436
|
+
exit_code=excluded.exit_code,
|
|
4437
|
+
summary_json=excluded.summary_json,
|
|
4438
|
+
evidence_paths_json=excluded.evidence_paths_json,
|
|
4439
|
+
updated_at=excluded.updated_at`).run({
|
|
4440
|
+
$runId: receipt.run_id,
|
|
4441
|
+
$loopId: receipt.loop_id,
|
|
4442
|
+
$machineJson: JSON.stringify(receipt.machine),
|
|
4443
|
+
$repo: receipt.repo,
|
|
4444
|
+
$taskIdsJson: JSON.stringify(receipt.task_ids),
|
|
4445
|
+
$knowledgeIdsJson: JSON.stringify(receipt.knowledge_ids),
|
|
4446
|
+
$digestId: receipt.digest_id,
|
|
4447
|
+
$startedAt: receipt.started_at,
|
|
4448
|
+
$finishedAt: receipt.finished_at,
|
|
4449
|
+
$status: receipt.status,
|
|
4450
|
+
$exitCode: receipt.exit_code,
|
|
4451
|
+
$summaryJson: JSON.stringify(receipt.summary),
|
|
4452
|
+
$evidencePathsJson: JSON.stringify(receipt.evidence_paths),
|
|
4453
|
+
$createdAt: receipt.created_at,
|
|
4454
|
+
$updatedAt: receipt.updated_at
|
|
4455
|
+
});
|
|
4456
|
+
return this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
4457
|
+
}
|
|
4458
|
+
getRunReceipt(runId) {
|
|
4459
|
+
const row = this.db.query("SELECT * FROM run_receipts WHERE run_id = ?").get(runId);
|
|
4460
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
4461
|
+
}
|
|
4462
|
+
listRunReceipts(opts = {}) {
|
|
4463
|
+
const limit = opts.limit ?? 100;
|
|
4464
|
+
const filters = [];
|
|
4465
|
+
const params = [];
|
|
4466
|
+
if (opts.loopId) {
|
|
4467
|
+
filters.push("loop_id = ?");
|
|
4468
|
+
params.push(opts.loopId);
|
|
4469
|
+
}
|
|
4470
|
+
if (opts.repo) {
|
|
4471
|
+
filters.push("repo = ?");
|
|
4472
|
+
params.push(opts.repo);
|
|
4473
|
+
}
|
|
4474
|
+
if (opts.status) {
|
|
4475
|
+
filters.push("status = ?");
|
|
4476
|
+
params.push(opts.status);
|
|
4477
|
+
}
|
|
4478
|
+
if (opts.taskId) {
|
|
4479
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.task_ids_json) WHERE value = ?)");
|
|
4480
|
+
params.push(opts.taskId);
|
|
4481
|
+
}
|
|
4482
|
+
if (opts.knowledgeId) {
|
|
4483
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.knowledge_ids_json) WHERE value = ?)");
|
|
4484
|
+
params.push(opts.knowledgeId);
|
|
4485
|
+
}
|
|
4486
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
4487
|
+
const rows = this.db.query(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
4488
|
+
return rows.map(rowToRunReceipt);
|
|
4489
|
+
}
|
|
3789
4490
|
deferLiveExpiredRun(id, now, opts = {}) {
|
|
3790
4491
|
const updated = now.toISOString();
|
|
3791
4492
|
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
@@ -3944,6 +4645,9 @@ class Store {
|
|
|
3944
4645
|
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
3945
4646
|
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
3946
4647
|
}
|
|
4648
|
+
exportMigrationRunPage(opts) {
|
|
4649
|
+
return this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC LIMIT ? OFFSET ?").all(opts.limit, opts.offset).map(rowToRun);
|
|
4650
|
+
}
|
|
3947
4651
|
countTable(table) {
|
|
3948
4652
|
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
3949
4653
|
return row?.count ?? 0;
|
|
@@ -4190,9 +4894,9 @@ class Store {
|
|
|
4190
4894
|
const runDir = dirname2(manifestPath);
|
|
4191
4895
|
try {
|
|
4192
4896
|
if (/^[0-9a-f]{12,64}$/.test(basename2(runDir))) {
|
|
4193
|
-
|
|
4897
|
+
rmSync3(runDir, { recursive: true, force: true });
|
|
4194
4898
|
} else {
|
|
4195
|
-
|
|
4899
|
+
rmSync3(manifestPath, { force: true });
|
|
4196
4900
|
}
|
|
4197
4901
|
} catch {}
|
|
4198
4902
|
}
|
|
@@ -4259,147 +4963,35 @@ class Store {
|
|
|
4259
4963
|
this.db.close();
|
|
4260
4964
|
if (this.memoryRootDir) {
|
|
4261
4965
|
try {
|
|
4262
|
-
|
|
4966
|
+
rmSync3(this.memoryRootDir, { recursive: true, force: true });
|
|
4263
4967
|
} catch {}
|
|
4264
4968
|
this.memoryRootDir = undefined;
|
|
4265
|
-
}
|
|
4266
|
-
}
|
|
4267
|
-
}
|
|
4268
|
-
|
|
4269
|
-
// src/daemon/index.ts
|
|
4270
|
-
import { Command } from "commander";
|
|
4271
|
-
|
|
4272
|
-
// src/daemon/daemon.ts
|
|
4273
|
-
import { copyFileSync, existsSync as existsSync5, openSync, renameSync as renameSync2, rmSync as rmSync4, statSync, truncateSync } from "fs";
|
|
4274
|
-
import { hostname as hostname2 } from "os";
|
|
4275
|
-
import { spawn as spawn3 } from "child_process";
|
|
4276
|
-
|
|
4277
|
-
// src/lib/health.ts
|
|
4278
|
-
import { createHash as createHash2 } from "crypto";
|
|
4279
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
4280
|
-
|
|
4281
|
-
// src/lib/format.ts
|
|
4282
|
-
var TEXT_OUTPUT_LIMIT = 32 * 1024;
|
|
4283
|
-
var SENSITIVE_PAYLOAD_KEYS = new Set(["env", "error", "prompt", "reason", "stderr", "stdout"]);
|
|
4284
|
-
function redact(value, visible = 0) {
|
|
4285
|
-
if (!value)
|
|
4286
|
-
return value;
|
|
4287
|
-
if (value.length <= visible)
|
|
4288
|
-
return value;
|
|
4289
|
-
if (visible <= 0)
|
|
4290
|
-
return `[redacted ${value.length} chars]`;
|
|
4291
|
-
return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
|
|
4292
|
-
}
|
|
4293
|
-
function truncateTextOutput(value) {
|
|
4294
|
-
if (value.length <= TEXT_OUTPUT_LIMIT)
|
|
4295
|
-
return value;
|
|
4296
|
-
return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
|
|
4297
|
-
[truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
|
|
4298
|
-
}
|
|
4299
|
-
function redactSensitivePayload(value, key) {
|
|
4300
|
-
if (key && SENSITIVE_PAYLOAD_KEYS.has(key)) {
|
|
4301
|
-
if (typeof value === "string")
|
|
4302
|
-
return redact(value);
|
|
4303
|
-
if (value === undefined || value === null)
|
|
4304
|
-
return value;
|
|
4305
|
-
return "[redacted]";
|
|
4306
|
-
}
|
|
4307
|
-
if (Array.isArray(value))
|
|
4308
|
-
return value.map((item) => redactSensitivePayload(item));
|
|
4309
|
-
if (value && typeof value === "object") {
|
|
4310
|
-
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactSensitivePayload(entryValue, entryKey)]));
|
|
4311
|
-
}
|
|
4312
|
-
return value;
|
|
4313
|
-
}
|
|
4314
|
-
function textOutputBlocks(value, opts = {}) {
|
|
4315
|
-
const indent = opts.indent ?? "";
|
|
4316
|
-
const nested = `${indent} `;
|
|
4317
|
-
const blocks = [];
|
|
4318
|
-
for (const [label, output] of [
|
|
4319
|
-
["stdout", value.stdout],
|
|
4320
|
-
["stderr", value.stderr]
|
|
4321
|
-
]) {
|
|
4322
|
-
if (!output)
|
|
4323
|
-
continue;
|
|
4324
|
-
blocks.push(`${indent}${label}:`);
|
|
4325
|
-
for (const line of truncateTextOutput(output).replace(/\s+$/, "").split(/\r?\n/)) {
|
|
4326
|
-
blocks.push(`${nested}${line}`);
|
|
4327
|
-
}
|
|
4328
|
-
}
|
|
4329
|
-
return blocks;
|
|
4330
|
-
}
|
|
4331
|
-
function publicLoop(loop) {
|
|
4332
|
-
const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : loop.target.type === "agent" ? { ...loop.target, prompt: redact(loop.target.prompt) } : loop.target;
|
|
4333
|
-
return {
|
|
4334
|
-
...loop,
|
|
4335
|
-
target
|
|
4336
|
-
};
|
|
4337
|
-
}
|
|
4338
|
-
function publicRun(run, showOutput = false, opts = {}) {
|
|
4339
|
-
return {
|
|
4340
|
-
...run,
|
|
4341
|
-
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
4342
|
-
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
4343
|
-
error: opts.redactError ? redact(run.error) : run.error
|
|
4344
|
-
};
|
|
4345
|
-
}
|
|
4346
|
-
function publicExecutorResult(result, showOutput = false) {
|
|
4347
|
-
return {
|
|
4348
|
-
...result,
|
|
4349
|
-
stdout: showOutput ? result.stdout : result.stdout ? `[redacted ${result.stdout.length} chars]` : undefined,
|
|
4350
|
-
stderr: showOutput ? result.stderr : result.stderr ? `[redacted ${result.stderr.length} chars]` : undefined,
|
|
4351
|
-
error: redact(result.error)
|
|
4352
|
-
};
|
|
4353
|
-
}
|
|
4354
|
-
function publicWorkflow(workflow) {
|
|
4355
|
-
return {
|
|
4356
|
-
...workflow,
|
|
4357
|
-
steps: workflow.steps.map((step) => ({
|
|
4358
|
-
...step,
|
|
4359
|
-
target: step.target.type === "agent" ? { ...step.target, prompt: redact(step.target.prompt) } : step.target.type === "command" && step.target.env ? { ...step.target, env: "[redacted]" } : step.target
|
|
4360
|
-
}))
|
|
4361
|
-
};
|
|
4362
|
-
}
|
|
4363
|
-
function publicWorkflowRun(run) {
|
|
4364
|
-
return { ...run, error: redact(run.error) };
|
|
4365
|
-
}
|
|
4366
|
-
function publicWorkflowInvocation(invocation) {
|
|
4367
|
-
return redactSensitivePayload(invocation);
|
|
4368
|
-
}
|
|
4369
|
-
function publicWorkflowWorkItem(item) {
|
|
4370
|
-
return { ...item, lastReason: redact(item.lastReason, 240) };
|
|
4371
|
-
}
|
|
4372
|
-
function publicWorkflowStepRun(run, showOutput = false) {
|
|
4373
|
-
return {
|
|
4374
|
-
...run,
|
|
4375
|
-
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
4376
|
-
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
4377
|
-
error: redact(run.error)
|
|
4378
|
-
};
|
|
4379
|
-
}
|
|
4380
|
-
function publicWorkflowEvent(event) {
|
|
4381
|
-
return { ...event, payload: redactSensitivePayload(event.payload) };
|
|
4382
|
-
}
|
|
4383
|
-
function publicGoal(goal) {
|
|
4384
|
-
return {
|
|
4385
|
-
...goal,
|
|
4386
|
-
objective: redact(goal.objective, 120)
|
|
4387
|
-
};
|
|
4388
|
-
}
|
|
4389
|
-
function publicGoalRun(run) {
|
|
4390
|
-
return {
|
|
4391
|
-
...run,
|
|
4392
|
-
evidence: redactSensitivePayload(run.evidence),
|
|
4393
|
-
rawResponse: redactSensitivePayload(run.rawResponse)
|
|
4394
|
-
};
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4395
4971
|
}
|
|
4396
4972
|
|
|
4973
|
+
// src/daemon/index.ts
|
|
4974
|
+
import { Command } from "commander";
|
|
4975
|
+
|
|
4976
|
+
// src/daemon/daemon.ts
|
|
4977
|
+
import { copyFileSync, existsSync as existsSync5, openSync as openSync2, renameSync as renameSync2, rmSync as rmSync5, statSync, truncateSync } from "fs";
|
|
4978
|
+
import { hostname as hostname3 } from "os";
|
|
4979
|
+
import { spawn as spawn3 } from "child_process";
|
|
4980
|
+
|
|
4397
4981
|
// src/lib/health.ts
|
|
4982
|
+
import { createHash as createHash3 } from "crypto";
|
|
4983
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
4984
|
+
import { join as join5 } from "path";
|
|
4398
4985
|
var EVIDENCE_CHARS = 2000;
|
|
4399
4986
|
var FINGERPRINT_EVIDENCE_CHARS = 120;
|
|
4987
|
+
var DEFAULT_SCAN_LIMIT = 200;
|
|
4988
|
+
var DEFAULT_SCAN_STATUSES = ["active", "paused"];
|
|
4989
|
+
var DEFAULT_MAX_FINDINGS = 100;
|
|
4990
|
+
var MIN_STALE_RUNNING_MS = 10 * 60000;
|
|
4400
4991
|
var CLASSIFICATIONS = [
|
|
4401
4992
|
"rate_limit",
|
|
4402
4993
|
"auth",
|
|
4994
|
+
"provider_unavailable",
|
|
4403
4995
|
"model_not_found",
|
|
4404
4996
|
"context_length",
|
|
4405
4997
|
"schema_response_format",
|
|
@@ -4408,10 +5000,12 @@ var CLASSIFICATIONS = [
|
|
|
4408
5000
|
"route_functional",
|
|
4409
5001
|
"timeout",
|
|
4410
5002
|
"sigsegv",
|
|
5003
|
+
"restart_interrupted",
|
|
4411
5004
|
"skipped_previous_active",
|
|
4412
5005
|
"circuit_breaker",
|
|
4413
5006
|
"unknown"
|
|
4414
5007
|
];
|
|
5008
|
+
var RESTART_INTERRUPTED_RUN_PREFIX = "daemon restart interrupted active run";
|
|
4415
5009
|
function bounded(value, limit = EVIDENCE_CHARS) {
|
|
4416
5010
|
if (!value)
|
|
4417
5011
|
return;
|
|
@@ -4425,7 +5019,7 @@ function redactedEvidence(value) {
|
|
|
4425
5019
|
}
|
|
4426
5020
|
function searchableText(run) {
|
|
4427
5021
|
return [run.error, run.stderr, run.stdout].filter(Boolean).join(`
|
|
4428
|
-
`)
|
|
5022
|
+
`);
|
|
4429
5023
|
}
|
|
4430
5024
|
function isRecord(value) {
|
|
4431
5025
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -4447,16 +5041,39 @@ function tagsFromValue(value) {
|
|
|
4447
5041
|
return [];
|
|
4448
5042
|
}
|
|
4449
5043
|
function stableFingerprint(parts) {
|
|
4450
|
-
return
|
|
5044
|
+
return createHash3("sha256").update(parts.join(`
|
|
4451
5045
|
`)).digest("hex").slice(0, 16);
|
|
4452
5046
|
}
|
|
4453
|
-
function
|
|
5047
|
+
function stableScanFingerprint(parts) {
|
|
5048
|
+
return `openloops:health-scan:${stableFingerprint(parts)}`;
|
|
5049
|
+
}
|
|
5050
|
+
function safeHost(value) {
|
|
5051
|
+
if (!value)
|
|
5052
|
+
return;
|
|
5053
|
+
let host = value.trim().replace(/^[a-z]+:\/\//i, "").split(/[/:?#\s)"'\\]+/)[0] ?? "";
|
|
5054
|
+
host = host.replace(/^\[|\]$/g, "");
|
|
5055
|
+
return /^[a-z0-9.-]+$/i.test(host) ? host.toLowerCase() : undefined;
|
|
5056
|
+
}
|
|
5057
|
+
function isCursorHost(host) {
|
|
5058
|
+
return host === "cursor.sh" || Boolean(host?.endsWith(".cursor.sh"));
|
|
5059
|
+
}
|
|
5060
|
+
function providerUnavailableSummary(rawText) {
|
|
5061
|
+
const dns = /\bgetaddrinfo\s+(EAI_AGAIN|ENOTFOUND)\s+([a-z0-9.-]+)/i.exec(rawText);
|
|
5062
|
+
if (dns) {
|
|
5063
|
+
const host = safeHost(dns[2]);
|
|
5064
|
+
if (isCursorHost(host))
|
|
5065
|
+
return `provider DNS lookup failed: ${dns[1]} ${host}`;
|
|
5066
|
+
}
|
|
5067
|
+
return;
|
|
5068
|
+
}
|
|
5069
|
+
function stableFailureFingerprint(run, classification, summary) {
|
|
5070
|
+
const evidence = summary ?? (classification === "provider_unavailable" ? providerUnavailableSummary(searchableText(run)) : undefined) ?? run.error ?? run.stderr ?? run.stdout ?? "";
|
|
4454
5071
|
return stableFingerprint([
|
|
4455
5072
|
run.loopId,
|
|
4456
5073
|
classification,
|
|
4457
5074
|
String(run.status),
|
|
4458
5075
|
String(run.exitCode ?? ""),
|
|
4459
|
-
|
|
5076
|
+
evidence.replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
|
|
4460
5077
|
]);
|
|
4461
5078
|
}
|
|
4462
5079
|
function stableRouteFunctionalFingerprint(loop, reason) {
|
|
@@ -4474,13 +5091,279 @@ function healthRun(run) {
|
|
|
4474
5091
|
stderr: redactedEvidence(run.stderr)
|
|
4475
5092
|
};
|
|
4476
5093
|
}
|
|
5094
|
+
function compactText(value, limit = 500) {
|
|
5095
|
+
const text = redact(bounded(value, limit));
|
|
5096
|
+
return text?.replace(/\s+/g, " ").trim();
|
|
5097
|
+
}
|
|
5098
|
+
function publicDoctorCheck(check) {
|
|
5099
|
+
return {
|
|
5100
|
+
id: check.id,
|
|
5101
|
+
status: check.status,
|
|
5102
|
+
message: redact(bounded(check.message, 500)) ?? "",
|
|
5103
|
+
detail: redact(bounded(check.detail, 800))
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
function publicDoctorReport(report) {
|
|
5107
|
+
return {
|
|
5108
|
+
ok: report.ok,
|
|
5109
|
+
checks: report.checks.map(publicDoctorCheck)
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
5112
|
+
function includedStatusSet(statuses) {
|
|
5113
|
+
const values = statuses?.length ? statuses : DEFAULT_SCAN_STATUSES;
|
|
5114
|
+
return new Set(values);
|
|
5115
|
+
}
|
|
5116
|
+
function statusCounts(loops) {
|
|
5117
|
+
return {
|
|
5118
|
+
loops: loops.length,
|
|
5119
|
+
active: loops.filter((loop) => loop.status === "active").length,
|
|
5120
|
+
paused: loops.filter((loop) => loop.status === "paused").length,
|
|
5121
|
+
stopped: loops.filter((loop) => loop.status === "stopped").length,
|
|
5122
|
+
expired: loops.filter((loop) => loop.status === "expired").length
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
function compareLoopsForScan(left, right) {
|
|
5126
|
+
const statusOrder = left.status.localeCompare(right.status);
|
|
5127
|
+
if (statusOrder !== 0)
|
|
5128
|
+
return statusOrder;
|
|
5129
|
+
return (left.nextRunAt ?? "").localeCompare(right.nextRunAt ?? "");
|
|
5130
|
+
}
|
|
5131
|
+
function scanLoops(store, statuses, opts) {
|
|
5132
|
+
const limit = opts.limit ?? DEFAULT_SCAN_LIMIT;
|
|
5133
|
+
return statuses.flatMap((status) => store.listLoops({ includeArchived: opts.includeArchived, status, limit })).sort(compareLoopsForScan).slice(0, limit);
|
|
5134
|
+
}
|
|
5135
|
+
function healthReportForLoops(store, loops, generatedAt) {
|
|
5136
|
+
const expectations = loops.map((loop) => expectationForLoop(store, loop));
|
|
5137
|
+
const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
|
|
5138
|
+
for (const expectation of expectations) {
|
|
5139
|
+
if (expectation.failure)
|
|
5140
|
+
classifications[expectation.failure.classification] += 1;
|
|
5141
|
+
}
|
|
5142
|
+
const unhealthy = expectations.filter((expectation) => !expectation.ok).length;
|
|
5143
|
+
const warnings = expectations.filter((expectation) => expectation.check.status === "warn").length;
|
|
5144
|
+
return {
|
|
5145
|
+
ok: unhealthy === 0,
|
|
5146
|
+
generatedAt,
|
|
5147
|
+
summary: {
|
|
5148
|
+
loops: expectations.length,
|
|
5149
|
+
healthy: expectations.length - unhealthy,
|
|
5150
|
+
unhealthy,
|
|
5151
|
+
warnings
|
|
5152
|
+
},
|
|
5153
|
+
classifications,
|
|
5154
|
+
expectations
|
|
5155
|
+
};
|
|
5156
|
+
}
|
|
5157
|
+
function ageMs(run, now) {
|
|
5158
|
+
const stamp = run.startedAt ?? run.createdAt ?? run.scheduledFor;
|
|
5159
|
+
const time = Date.parse(stamp);
|
|
5160
|
+
return Number.isFinite(time) ? Math.max(0, now.getTime() - time) : 0;
|
|
5161
|
+
}
|
|
5162
|
+
function shortLoop(loop) {
|
|
5163
|
+
return {
|
|
5164
|
+
id: loop.id,
|
|
5165
|
+
name: loop.name,
|
|
5166
|
+
status: loop.status,
|
|
5167
|
+
nextRunAt: loop.nextRunAt,
|
|
5168
|
+
leaseMs: loop.leaseMs
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
function priorityForSeverity(severity) {
|
|
5172
|
+
if (severity === "critical")
|
|
5173
|
+
return "critical";
|
|
5174
|
+
if (severity === "high")
|
|
5175
|
+
return "high";
|
|
5176
|
+
if (severity === "low")
|
|
5177
|
+
return "low";
|
|
5178
|
+
return "medium";
|
|
5179
|
+
}
|
|
5180
|
+
function recommendedFindingTask(finding, route) {
|
|
5181
|
+
const tags = ["bug", "openloops", "loops", "loop-health", finding.kind];
|
|
5182
|
+
if (finding.classification)
|
|
5183
|
+
tags.push(finding.classification);
|
|
5184
|
+
const description = [
|
|
5185
|
+
`OpenLoops health scan found a ${finding.kind} issue.`,
|
|
5186
|
+
finding.loop ? `Loop: ${finding.loop.name} (${finding.loop.id})` : undefined,
|
|
5187
|
+
finding.run ? `Run: ${finding.run.id}` : undefined,
|
|
5188
|
+
finding.classification ? `Classification: ${finding.classification}` : undefined,
|
|
5189
|
+
finding.ageMs !== undefined ? `AgeMs: ${finding.ageMs}` : undefined,
|
|
5190
|
+
finding.staleThresholdMs !== undefined ? `StaleThresholdMs: ${finding.staleThresholdMs}` : undefined,
|
|
5191
|
+
`Fingerprint: ${finding.fingerprint}`,
|
|
5192
|
+
`Severity: ${finding.severity}`,
|
|
5193
|
+
`No-tmux routing: Do not dispatch or paste prompts into tmux panes; use task-triggered headless worker/verifier workflows only.`,
|
|
5194
|
+
route?.cwd ? `Route cwd: ${route.cwd}` : undefined,
|
|
5195
|
+
route?.provider ? `Provider: ${route.provider}` : undefined,
|
|
5196
|
+
"",
|
|
5197
|
+
finding.message,
|
|
5198
|
+
finding.run?.error ? `Error:
|
|
5199
|
+
${finding.run.error}` : undefined,
|
|
5200
|
+
finding.run?.stderr ? `Stderr:
|
|
5201
|
+
${finding.run.stderr}` : undefined
|
|
5202
|
+
].filter(Boolean).join(`
|
|
5203
|
+
|
|
5204
|
+
`);
|
|
5205
|
+
return {
|
|
5206
|
+
title: finding.title,
|
|
5207
|
+
description,
|
|
5208
|
+
priority: priorityForSeverity(finding.severity),
|
|
5209
|
+
tags,
|
|
5210
|
+
dedupeKey: finding.fingerprint,
|
|
5211
|
+
search: { query: finding.fingerprint },
|
|
5212
|
+
compatibilityFallback: {
|
|
5213
|
+
search: ["todos", "search", finding.fingerprint, "--json"],
|
|
5214
|
+
add: ["todos", "add", finding.title, "--description", description, "--tag", tags.join(","), "--priority", priorityForSeverity(finding.severity)],
|
|
5215
|
+
comment: ["todos", "comment", "<task-id>", description]
|
|
5216
|
+
},
|
|
5217
|
+
futureNativeUpsert: {
|
|
5218
|
+
command: "todos task upsert",
|
|
5219
|
+
fields: {
|
|
5220
|
+
title: finding.title,
|
|
5221
|
+
description,
|
|
5222
|
+
priority: priorityForSeverity(finding.severity),
|
|
5223
|
+
tags,
|
|
5224
|
+
dedupeKey: finding.fingerprint,
|
|
5225
|
+
routeSource: route?.source ?? "openloops",
|
|
5226
|
+
routeKind: route?.kind ?? "health_scan",
|
|
5227
|
+
routeLoopId: finding.loop?.id ?? "",
|
|
5228
|
+
routeLoopName: finding.loop?.name ?? ""
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
function daemonFinding(daemon) {
|
|
5234
|
+
if (daemon.running && !daemon.stale)
|
|
5235
|
+
return;
|
|
5236
|
+
const reason = daemon.stale ? "daemon pid file is stale" : "daemon is not running";
|
|
5237
|
+
const severity = "critical";
|
|
5238
|
+
const finding = {
|
|
5239
|
+
kind: "daemon",
|
|
5240
|
+
severity,
|
|
5241
|
+
fingerprint: `openloops:health-scan:daemon:${daemon.stale ? "stale" : "not-running"}`,
|
|
5242
|
+
title: "OpenLoops daemon health issue",
|
|
5243
|
+
message: reason
|
|
5244
|
+
};
|
|
5245
|
+
return {
|
|
5246
|
+
...finding,
|
|
5247
|
+
recommendedTask: recommendedFindingTask(finding, undefined)
|
|
5248
|
+
};
|
|
5249
|
+
}
|
|
5250
|
+
function doctorSeverity(check) {
|
|
5251
|
+
if (check.status === "fail" && check.id === "data-dir")
|
|
5252
|
+
return "critical";
|
|
5253
|
+
if (check.status === "fail")
|
|
5254
|
+
return "high";
|
|
5255
|
+
return "medium";
|
|
5256
|
+
}
|
|
5257
|
+
function doctorFinding(check, loop, route) {
|
|
5258
|
+
if (check.status === "ok" || check.id === "loop-runs")
|
|
5259
|
+
return;
|
|
5260
|
+
const kind = check.id.startsWith("loop:") && check.id.endsWith(":preflight") ? "preflight" : "doctor";
|
|
5261
|
+
const severity = kind === "preflight" && check.status === "fail" ? "high" : doctorSeverity(check);
|
|
5262
|
+
const fingerprint = stableScanFingerprint(["doctor", check.id, check.status, check.message, check.detail ?? ""]);
|
|
5263
|
+
const finding = {
|
|
5264
|
+
kind,
|
|
5265
|
+
severity,
|
|
5266
|
+
fingerprint,
|
|
5267
|
+
title: kind === "preflight" && loop ? `OpenLoops preflight issue - ${loop.name}` : `OpenLoops doctor issue - ${check.id}`,
|
|
5268
|
+
message: [check.status, check.message, check.detail].filter(Boolean).join(" "),
|
|
5269
|
+
loop: loop ? shortLoop(loop) : undefined,
|
|
5270
|
+
route,
|
|
5271
|
+
doctorCheck: publicDoctorCheck(check)
|
|
5272
|
+
};
|
|
5273
|
+
return {
|
|
5274
|
+
...finding,
|
|
5275
|
+
recommendedTask: recommendedFindingTask(finding, route)
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
function latestRunFinding(expectation) {
|
|
5279
|
+
if (expectation.ok || !expectation.latestRun || expectation.latestRun.status === "running")
|
|
5280
|
+
return;
|
|
5281
|
+
const failure = expectation.failure;
|
|
5282
|
+
if (!failure)
|
|
5283
|
+
return;
|
|
5284
|
+
const severity = expectation.loop.status === "active" ? "high" : "medium";
|
|
5285
|
+
return {
|
|
5286
|
+
kind: "latest-run",
|
|
5287
|
+
severity,
|
|
5288
|
+
fingerprint: expectation.recommendedTask?.dedupeKey ?? `openloops:${expectation.loop.id}:${failure.fingerprint}`,
|
|
5289
|
+
title: expectation.recommendedTask?.title ?? `OpenLoops latest run failed - ${expectation.loop.name}`,
|
|
5290
|
+
message: expectation.check.message,
|
|
5291
|
+
loop: expectation.loop,
|
|
5292
|
+
run: expectation.latestRun,
|
|
5293
|
+
route: expectation.route,
|
|
5294
|
+
classification: failure.classification,
|
|
5295
|
+
doctorCheck: undefined,
|
|
5296
|
+
recommendedTask: expectation.recommendedTask
|
|
5297
|
+
};
|
|
5298
|
+
}
|
|
5299
|
+
function staleRunningFinding(loop, expectation, now, staleRunningMs) {
|
|
5300
|
+
const run = expectation.latestRun;
|
|
5301
|
+
if (loop.status !== "active" || run?.status !== "running")
|
|
5302
|
+
return;
|
|
5303
|
+
const threshold = Math.max(loop.leaseMs, staleRunningMs, MIN_STALE_RUNNING_MS);
|
|
5304
|
+
const age = ageMs(run, now);
|
|
5305
|
+
if (age <= threshold)
|
|
5306
|
+
return;
|
|
5307
|
+
const fingerprint = `openloops:health-scan:stale-running:${loop.id}:${run.id}`;
|
|
5308
|
+
const message = `active loop latest run is still running after ${age}ms (threshold ${threshold}ms)`;
|
|
5309
|
+
const finding = {
|
|
5310
|
+
kind: "stale-running",
|
|
5311
|
+
severity: "critical",
|
|
5312
|
+
fingerprint,
|
|
5313
|
+
title: `OpenLoops stale running run - ${loop.name}`,
|
|
5314
|
+
message,
|
|
5315
|
+
loop: shortLoop(loop),
|
|
5316
|
+
run,
|
|
5317
|
+
route: expectation.route,
|
|
5318
|
+
ageMs: age,
|
|
5319
|
+
staleThresholdMs: threshold
|
|
5320
|
+
};
|
|
5321
|
+
return {
|
|
5322
|
+
...finding,
|
|
5323
|
+
recommendedTask: recommendedFindingTask(finding, expectation.route)
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
5326
|
+
function scanStatus(findings) {
|
|
5327
|
+
if (findings.some((finding) => finding.severity === "critical"))
|
|
5328
|
+
return "critical";
|
|
5329
|
+
if (findings.length > 0)
|
|
5330
|
+
return "degraded";
|
|
5331
|
+
return "ok";
|
|
5332
|
+
}
|
|
5333
|
+
function timestampDir(root, generatedAt) {
|
|
5334
|
+
const stamp = generatedAt.replace(/[-:]/g, "").replace(/\./g, "");
|
|
5335
|
+
return join5(root, stamp);
|
|
5336
|
+
}
|
|
5337
|
+
function healthScanMarkdown(scan) {
|
|
5338
|
+
return [
|
|
5339
|
+
"# OpenLoops Health Scan",
|
|
5340
|
+
"",
|
|
5341
|
+
`- status: ${scan.status}`,
|
|
5342
|
+
`- generated_at: ${scan.generatedAt}`,
|
|
5343
|
+
`- included_statuses: ${scan.includedStatuses.join(",")}`,
|
|
5344
|
+
`- loops: total=${scan.counts.loops} active=${scan.counts.active} paused=${scan.counts.paused} stopped=${scan.counts.stopped} expired=${scan.counts.expired}`,
|
|
5345
|
+
`- findings: total=${scan.counts.findings} reported=${scan.counts.reportedFindings} truncated=${scan.counts.truncatedFindings} latest_run=${scan.counts.latestRunFindings} stale_running=${scan.counts.staleRunning} daemon=${scan.counts.daemonFindings} doctor=${scan.counts.doctorFindings} preflight=${scan.counts.preflightFindings}`,
|
|
5346
|
+
scan.daemon ? `- daemon: running=${scan.daemon.running} stale=${scan.daemon.stale} pid=${scan.daemon.pid ?? "none"}` : "- daemon: not checked",
|
|
5347
|
+
scan.doctor ? `- doctor_ok: ${scan.doctor.ok}` : "- doctor: not checked",
|
|
5348
|
+
scan.selfHeals.length ? `- self_heals: ${scan.selfHeals.map((action) => `${action.kind}:${action.attempted ? action.ok ? "ok" : "failed" : "skipped"}`).join(",")}` : "- self_heals: none",
|
|
5349
|
+
"",
|
|
5350
|
+
"## Findings",
|
|
5351
|
+
scan.findings.length ? scan.findings.map((finding) => `- ${finding.severity} ${finding.kind} ${finding.fingerprint} ${finding.loop ? `${finding.loop.name}: ` : ""}${compactText(finding.message, 240) ?? ""}`).join(`
|
|
5352
|
+
`) : "None."
|
|
5353
|
+
].join(`
|
|
5354
|
+
`);
|
|
5355
|
+
}
|
|
4477
5356
|
function classifyRunFailure(run) {
|
|
4478
5357
|
if (run.status === "succeeded" || run.status === "running")
|
|
4479
5358
|
return;
|
|
4480
|
-
const
|
|
5359
|
+
const rawText = searchableText(run);
|
|
5360
|
+
const text = rawText.toLowerCase();
|
|
4481
5361
|
let classification = "unknown";
|
|
5362
|
+
let summary;
|
|
4482
5363
|
if (run.status === "timed_out")
|
|
4483
5364
|
classification = "timeout";
|
|
5365
|
+
else if (run.status === "skipped" && run.error?.startsWith(RESTART_INTERRUPTED_RUN_PREFIX))
|
|
5366
|
+
classification = "restart_interrupted";
|
|
4484
5367
|
else if (run.status === "skipped" && /circuit breaker open/.test(text))
|
|
4485
5368
|
classification = "circuit_breaker";
|
|
4486
5369
|
else if (run.status === "skipped" && /previous run still active/.test(text))
|
|
@@ -4489,8 +5372,10 @@ function classifyRunFailure(run) {
|
|
|
4489
5372
|
classification = "preflight";
|
|
4490
5373
|
else if (/rate limit|too many requests|429\b|quota exceeded/.test(text))
|
|
4491
5374
|
classification = "rate_limit";
|
|
4492
|
-
else if (/unauthorized|authentication|auth\b|api key|invalid token|permission denied|401\b|403\b/.test(text))
|
|
5375
|
+
else if (/unauthorized|authentication|auth\b|api key|invalid token|permission denied|401\b|403\b|not logged in|please run \/login|login required|not authenticated|failed to authenticate/.test(text))
|
|
4493
5376
|
classification = "auth";
|
|
5377
|
+
else if (summary = providerUnavailableSummary(rawText))
|
|
5378
|
+
classification = "provider_unavailable";
|
|
4494
5379
|
else if (/model .*not found|model_not_found|unknown model|invalid model|404.*model/.test(text))
|
|
4495
5380
|
classification = "model_not_found";
|
|
4496
5381
|
else if (/context length|context_length|context window|maximum context|token limit|too many tokens/.test(text))
|
|
@@ -4503,8 +5388,9 @@ function classifyRunFailure(run) {
|
|
|
4503
5388
|
classification = "sigsegv";
|
|
4504
5389
|
return {
|
|
4505
5390
|
classification,
|
|
4506
|
-
fingerprint: stableFailureFingerprint(run, classification),
|
|
5391
|
+
fingerprint: stableFailureFingerprint(run, classification, summary),
|
|
4507
5392
|
evidence: {
|
|
5393
|
+
summary,
|
|
4508
5394
|
error: redactedEvidence(run.error),
|
|
4509
5395
|
stdout: redactedEvidence(run.stdout),
|
|
4510
5396
|
stderr: redactedEvidence(run.stderr),
|
|
@@ -4549,7 +5435,7 @@ function routeEvidenceReport(run) {
|
|
|
4549
5435
|
const evidencePath = stringValue(stdoutReport?.evidencePath);
|
|
4550
5436
|
if (evidencePath && existsSync2(evidencePath)) {
|
|
4551
5437
|
try {
|
|
4552
|
-
return parseJsonObject(
|
|
5438
|
+
return parseJsonObject(readFileSync4(evidencePath, "utf8")) ?? stdoutReport;
|
|
4553
5439
|
} catch {
|
|
4554
5440
|
return stdoutReport;
|
|
4555
5441
|
}
|
|
@@ -4681,6 +5567,12 @@ function targetRoute(loop) {
|
|
|
4681
5567
|
loopName: loop.name
|
|
4682
5568
|
};
|
|
4683
5569
|
}
|
|
5570
|
+
function expectationLoop(loop) {
|
|
5571
|
+
return { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt, retryScheduledFor: loop.retryScheduledFor };
|
|
5572
|
+
}
|
|
5573
|
+
function hasPendingRetry(loop, run) {
|
|
5574
|
+
return loop.status === "active" && loop.retryScheduledFor === run.scheduledFor && run.attempt < loop.maxAttempts;
|
|
5575
|
+
}
|
|
4684
5576
|
function recommendedTask(loop, run, failure, route) {
|
|
4685
5577
|
const title = `BUG: open-loops loop failure - ${loop.name}`;
|
|
4686
5578
|
const description = [
|
|
@@ -4692,6 +5584,7 @@ function recommendedTask(loop, run, failure, route) {
|
|
|
4692
5584
|
`No-tmux routing: Do not dispatch or paste prompts into tmux panes; use task-triggered headless worker/verifier workflows only.`,
|
|
4693
5585
|
route.cwd ? `Route cwd: ${route.cwd}` : undefined,
|
|
4694
5586
|
route.provider ? `Provider: ${route.provider}` : undefined,
|
|
5587
|
+
failure.evidence.summary ? `Summary: ${failure.evidence.summary}` : undefined,
|
|
4695
5588
|
failure.evidence.error ? `Error:
|
|
4696
5589
|
${failure.evidence.error}` : undefined,
|
|
4697
5590
|
failure.evidence.stderr ? `Stderr:
|
|
@@ -4701,7 +5594,7 @@ ${failure.evidence.stderr}` : undefined
|
|
|
4701
5594
|
`);
|
|
4702
5595
|
const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
|
|
4703
5596
|
const tags = ["bug", "openloops", "loops", "loop-health", failure.classification];
|
|
4704
|
-
const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
|
|
5597
|
+
const priority = failure.classification === "auth" || failure.classification === "rate_limit" || failure.classification === "provider_unavailable" ? "high" : "medium";
|
|
4705
5598
|
return {
|
|
4706
5599
|
title,
|
|
4707
5600
|
description,
|
|
@@ -4735,7 +5628,7 @@ function expectationForLoop(store, loop) {
|
|
|
4735
5628
|
const route = targetRoute(loop);
|
|
4736
5629
|
if (!latestRun) {
|
|
4737
5630
|
return {
|
|
4738
|
-
loop:
|
|
5631
|
+
loop: expectationLoop(loop),
|
|
4739
5632
|
ok: true,
|
|
4740
5633
|
check: { id: "latest-run-succeeded", status: "warn", message: "loop has no recorded runs yet" },
|
|
4741
5634
|
route
|
|
@@ -4744,7 +5637,7 @@ function expectationForLoop(store, loop) {
|
|
|
4744
5637
|
const routeFailure = detectRouteFunctionalFailure(store, loop, latestRun);
|
|
4745
5638
|
if (routeFailure) {
|
|
4746
5639
|
return {
|
|
4747
|
-
loop:
|
|
5640
|
+
loop: expectationLoop(loop),
|
|
4748
5641
|
ok: false,
|
|
4749
5642
|
check: { id: "route-functional-health", status: "fail", message: routeFailure.evidence.error ?? "route functional blocker detected" },
|
|
4750
5643
|
latestRun: healthRun(latestRun),
|
|
@@ -4755,7 +5648,7 @@ function expectationForLoop(store, loop) {
|
|
|
4755
5648
|
}
|
|
4756
5649
|
if (latestRun.status === "succeeded") {
|
|
4757
5650
|
return {
|
|
4758
|
-
loop:
|
|
5651
|
+
loop: expectationLoop(loop),
|
|
4759
5652
|
ok: true,
|
|
4760
5653
|
check: { id: "latest-run-succeeded", status: "pass", message: "latest run succeeded" },
|
|
4761
5654
|
latestRun: healthRun(latestRun),
|
|
@@ -4766,7 +5659,7 @@ function expectationForLoop(store, loop) {
|
|
|
4766
5659
|
if (failure?.classification === "circuit_breaker") {
|
|
4767
5660
|
if (loop.status !== "paused") {
|
|
4768
5661
|
return {
|
|
4769
|
-
loop:
|
|
5662
|
+
loop: expectationLoop(loop),
|
|
4770
5663
|
ok: true,
|
|
4771
5664
|
check: { id: "latest-run-succeeded", status: "warn", message: "circuit breaker cleared by resume; awaiting next run" },
|
|
4772
5665
|
latestRun: healthRun(latestRun),
|
|
@@ -4774,7 +5667,7 @@ function expectationForLoop(store, loop) {
|
|
|
4774
5667
|
};
|
|
4775
5668
|
}
|
|
4776
5669
|
return {
|
|
4777
|
-
loop:
|
|
5670
|
+
loop: expectationLoop(loop),
|
|
4778
5671
|
ok: false,
|
|
4779
5672
|
check: { id: "latest-run-succeeded", status: "fail", message: latestRun.error ?? "circuit breaker open; loop auto-paused" },
|
|
4780
5673
|
latestRun: healthRun(latestRun),
|
|
@@ -4783,8 +5676,33 @@ function expectationForLoop(store, loop) {
|
|
|
4783
5676
|
recommendedTask: recommendedTask(loop, latestRun, failure, route)
|
|
4784
5677
|
};
|
|
4785
5678
|
}
|
|
5679
|
+
if (failure?.classification === "restart_interrupted") {
|
|
5680
|
+
return {
|
|
5681
|
+
loop: expectationLoop(loop),
|
|
5682
|
+
ok: true,
|
|
5683
|
+
check: { id: "latest-run-succeeded", status: "warn", message: latestRun.error ?? "daemon restart interrupted latest run" },
|
|
5684
|
+
latestRun: healthRun(latestRun),
|
|
5685
|
+
failure,
|
|
5686
|
+
route
|
|
5687
|
+
};
|
|
5688
|
+
}
|
|
5689
|
+
if (failure?.classification === "provider_unavailable" && hasPendingRetry(loop, latestRun)) {
|
|
5690
|
+
const message = [
|
|
5691
|
+
"provider unavailable/network failure; retry is scheduled",
|
|
5692
|
+
loop.nextRunAt ? `next attempt at ${loop.nextRunAt}` : undefined,
|
|
5693
|
+
failure.evidence.summary
|
|
5694
|
+
].filter(Boolean).join("; ");
|
|
5695
|
+
return {
|
|
5696
|
+
loop: expectationLoop(loop),
|
|
5697
|
+
ok: true,
|
|
5698
|
+
check: { id: "latest-run-succeeded", status: "warn", message },
|
|
5699
|
+
latestRun: healthRun(latestRun),
|
|
5700
|
+
failure,
|
|
5701
|
+
route
|
|
5702
|
+
};
|
|
5703
|
+
}
|
|
4786
5704
|
return {
|
|
4787
|
-
loop:
|
|
5705
|
+
loop: expectationLoop(loop),
|
|
4788
5706
|
ok: false,
|
|
4789
5707
|
check: { id: "latest-run-succeeded", status: "fail", message: `latest run is ${latestRun.status}` },
|
|
4790
5708
|
latestRun: healthRun(latestRun),
|
|
@@ -4816,16 +5734,99 @@ function buildHealthReport(store, opts = {}) {
|
|
|
4816
5734
|
expectations
|
|
4817
5735
|
};
|
|
4818
5736
|
}
|
|
5737
|
+
function buildHealthScan(store, opts = {}) {
|
|
5738
|
+
const generatedAt = (opts.now ?? new Date).toISOString();
|
|
5739
|
+
const now = opts.now ?? new Date(generatedAt);
|
|
5740
|
+
const includeStatuses = [...includedStatusSet(opts.includeStatuses)];
|
|
5741
|
+
const loops = scanLoops(store, includeStatuses, opts);
|
|
5742
|
+
const health = healthReportForLoops(store, loops, generatedAt);
|
|
5743
|
+
const expectationsByLoopId = new Map(health.expectations.map((expectation) => [expectation.loop.id, expectation]));
|
|
5744
|
+
const loopsById = new Map(loops.map((loop) => [loop.id, loop]));
|
|
5745
|
+
const maxFindings = Math.max(0, opts.maxFindings ?? DEFAULT_MAX_FINDINGS);
|
|
5746
|
+
const allFindings = [];
|
|
5747
|
+
const pushFinding = (finding) => {
|
|
5748
|
+
if (!finding)
|
|
5749
|
+
return;
|
|
5750
|
+
allFindings.push(finding);
|
|
5751
|
+
};
|
|
5752
|
+
if (opts.daemon)
|
|
5753
|
+
pushFinding(daemonFinding(opts.daemon));
|
|
5754
|
+
if (opts.latestRun !== false) {
|
|
5755
|
+
for (const loop of loops) {
|
|
5756
|
+
const expectation = expectationsByLoopId.get(loop.id);
|
|
5757
|
+
if (!expectation)
|
|
5758
|
+
continue;
|
|
5759
|
+
pushFinding(staleRunningFinding(loop, expectation, now, opts.staleRunningMs ?? MIN_STALE_RUNNING_MS));
|
|
5760
|
+
pushFinding(latestRunFinding(expectation));
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
const doctor = opts.doctor ? publicDoctorReport(opts.doctor) : undefined;
|
|
5764
|
+
if (doctor) {
|
|
5765
|
+
for (const check of doctor.checks) {
|
|
5766
|
+
const preflightLoopId = check.id.startsWith("loop:") && check.id.endsWith(":preflight") ? check.id.slice("loop:".length, -":preflight".length) : undefined;
|
|
5767
|
+
const loop = preflightLoopId ? loopsById.get(preflightLoopId) : undefined;
|
|
5768
|
+
const route = preflightLoopId ? expectationsByLoopId.get(preflightLoopId)?.route : undefined;
|
|
5769
|
+
pushFinding(doctorFinding(check, loop, route));
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
const findings = allFindings.slice(0, maxFindings);
|
|
5773
|
+
const status = scanStatus(allFindings);
|
|
5774
|
+
const baseCounts = statusCounts(loops);
|
|
5775
|
+
return {
|
|
5776
|
+
ok: status === "ok",
|
|
5777
|
+
status,
|
|
5778
|
+
generatedAt,
|
|
5779
|
+
includedStatuses: includeStatuses,
|
|
5780
|
+
counts: {
|
|
5781
|
+
...baseCounts,
|
|
5782
|
+
latestRunFindings: allFindings.filter((finding) => finding.kind === "latest-run").length,
|
|
5783
|
+
staleRunning: allFindings.filter((finding) => finding.kind === "stale-running").length,
|
|
5784
|
+
daemonFindings: allFindings.filter((finding) => finding.kind === "daemon").length,
|
|
5785
|
+
doctorFindings: allFindings.filter((finding) => finding.kind === "doctor").length,
|
|
5786
|
+
preflightFindings: allFindings.filter((finding) => finding.kind === "preflight").length,
|
|
5787
|
+
findings: allFindings.length,
|
|
5788
|
+
reportedFindings: findings.length,
|
|
5789
|
+
truncatedFindings: Math.max(0, allFindings.length - findings.length)
|
|
5790
|
+
},
|
|
5791
|
+
daemon: opts.daemon ? {
|
|
5792
|
+
running: opts.daemon.running,
|
|
5793
|
+
stale: opts.daemon.stale,
|
|
5794
|
+
pid: opts.daemon.pid,
|
|
5795
|
+
host: opts.daemon.host,
|
|
5796
|
+
loops: opts.daemon.loops,
|
|
5797
|
+
runs: opts.daemon.runs,
|
|
5798
|
+
logPath: opts.daemon.logPath
|
|
5799
|
+
} : undefined,
|
|
5800
|
+
doctor,
|
|
5801
|
+
health,
|
|
5802
|
+
selfHeals: opts.selfHeals ?? [],
|
|
5803
|
+
findings
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
function writeHealthScanReports(scan, opts = {}) {
|
|
5807
|
+
const root = opts.reportDir ?? join5(dataDir(), "reports", "health-scan");
|
|
5808
|
+
const dir = timestampDir(root, scan.generatedAt);
|
|
5809
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
5810
|
+
const json = join5(dir, "summary.json");
|
|
5811
|
+
const markdown = join5(dir, "report.md");
|
|
5812
|
+
const withReports = {
|
|
5813
|
+
...scan,
|
|
5814
|
+
reports: { dir, json, markdown }
|
|
5815
|
+
};
|
|
5816
|
+
writeFileSync2(json, JSON.stringify(withReports, null, 2), { mode: 384 });
|
|
5817
|
+
writeFileSync2(markdown, healthScanMarkdown(withReports), { mode: 384 });
|
|
5818
|
+
return withReports;
|
|
5819
|
+
}
|
|
4819
5820
|
|
|
4820
5821
|
// src/lib/executor.ts
|
|
4821
|
-
import { spawn as spawn2, spawnSync as
|
|
5822
|
+
import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
|
|
4822
5823
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
4823
5824
|
import { once } from "events";
|
|
4824
|
-
import { lstatSync, mkdirSync as
|
|
5825
|
+
import { lstatSync, mkdirSync as mkdirSync5, realpathSync } from "fs";
|
|
4825
5826
|
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
4826
5827
|
|
|
4827
5828
|
// src/lib/accounts.ts
|
|
4828
|
-
import { spawnSync as
|
|
5829
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4829
5830
|
import { existsSync as existsSync3 } from "fs";
|
|
4830
5831
|
var EXPORT_RE = /^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
4831
5832
|
var ACCOUNTS_ENV_TIMEOUT_MS = 30000;
|
|
@@ -4930,7 +5931,7 @@ function resolveAccountEnvSync(account, toolHint, env) {
|
|
|
4930
5931
|
if (!account)
|
|
4931
5932
|
return {};
|
|
4932
5933
|
const tool = requiredAccountTool(account, toolHint);
|
|
4933
|
-
const result =
|
|
5934
|
+
const result = spawnSync3("accounts", ["env", account.profile, "--tool", tool], {
|
|
4934
5935
|
encoding: "utf8",
|
|
4935
5936
|
env,
|
|
4936
5937
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4946,8 +5947,8 @@ function resolveAccountEnvSync(account, toolHint, env) {
|
|
|
4946
5947
|
|
|
4947
5948
|
// src/lib/env.ts
|
|
4948
5949
|
import { accessSync, constants } from "fs";
|
|
4949
|
-
import { homedir as
|
|
4950
|
-
import { delimiter, join as
|
|
5950
|
+
import { homedir as homedir3 } from "os";
|
|
5951
|
+
import { delimiter, join as join6 } from "path";
|
|
4951
5952
|
function compactPathParts(parts) {
|
|
4952
5953
|
const seen = new Set;
|
|
4953
5954
|
const result = [];
|
|
@@ -4961,16 +5962,16 @@ function compactPathParts(parts) {
|
|
|
4961
5962
|
return result;
|
|
4962
5963
|
}
|
|
4963
5964
|
function commonExecutableDirs(env = process.env) {
|
|
4964
|
-
const home = env.HOME ||
|
|
5965
|
+
const home = env.HOME || homedir3();
|
|
4965
5966
|
return compactPathParts([
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
env.BUN_INSTALL ?
|
|
5967
|
+
join6(home, ".local", "bin"),
|
|
5968
|
+
join6(home, ".bun", "bin"),
|
|
5969
|
+
join6(home, ".cargo", "bin"),
|
|
5970
|
+
join6(home, ".npm-global", "bin"),
|
|
5971
|
+
join6(home, "bin"),
|
|
5972
|
+
env.BUN_INSTALL ? join6(env.BUN_INSTALL, "bin") : undefined,
|
|
4972
5973
|
env.PNPM_HOME,
|
|
4973
|
-
env.NPM_CONFIG_PREFIX ?
|
|
5974
|
+
env.NPM_CONFIG_PREFIX ? join6(env.NPM_CONFIG_PREFIX, "bin") : undefined,
|
|
4974
5975
|
"/opt/homebrew/bin",
|
|
4975
5976
|
"/usr/local/bin",
|
|
4976
5977
|
"/usr/bin",
|
|
@@ -4994,7 +5995,7 @@ function executableExists(command, env = process.env) {
|
|
|
4994
5995
|
if (command.includes("/"))
|
|
4995
5996
|
return isExecutable(command);
|
|
4996
5997
|
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
4997
|
-
if (dir && isExecutable(
|
|
5998
|
+
if (dir && isExecutable(join6(dir, command)))
|
|
4998
5999
|
return true;
|
|
4999
6000
|
}
|
|
5000
6001
|
return false;
|
|
@@ -5103,6 +6104,7 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
5103
6104
|
"LOGNAME",
|
|
5104
6105
|
"PATH",
|
|
5105
6106
|
"SHELL",
|
|
6107
|
+
"SHLVL",
|
|
5106
6108
|
"SSH_AGENT_PID",
|
|
5107
6109
|
"SSH_AUTH_SOCK",
|
|
5108
6110
|
"TERM",
|
|
@@ -5135,6 +6137,32 @@ function timeoutResult(startedAt, error, fields = {}) {
|
|
|
5135
6137
|
function successResult(startedAt, fields = {}) {
|
|
5136
6138
|
return buildResult("succeeded", startedAt, fields);
|
|
5137
6139
|
}
|
|
6140
|
+
function codewithJsonlHasTerminalSuccess(stdout) {
|
|
6141
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
6142
|
+
const trimmed = line.trim();
|
|
6143
|
+
if (!trimmed || !trimmed.startsWith("{"))
|
|
6144
|
+
continue;
|
|
6145
|
+
let event;
|
|
6146
|
+
try {
|
|
6147
|
+
event = JSON.parse(trimmed);
|
|
6148
|
+
} catch {
|
|
6149
|
+
continue;
|
|
6150
|
+
}
|
|
6151
|
+
if (!event || typeof event !== "object")
|
|
6152
|
+
continue;
|
|
6153
|
+
const record = event;
|
|
6154
|
+
if (record.type === "task_complete")
|
|
6155
|
+
return true;
|
|
6156
|
+
const payload = record.payload;
|
|
6157
|
+
if (payload && typeof payload === "object" && payload.type === "task_complete") {
|
|
6158
|
+
return true;
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
return false;
|
|
6162
|
+
}
|
|
6163
|
+
function codewithJsonlReconciledSuccess(spec, fields) {
|
|
6164
|
+
return spec.agentProvider === "codewith" && codewithJsonlHasTerminalSuccess(fields.stdout ?? "");
|
|
6165
|
+
}
|
|
5138
6166
|
function notifySpawn(pid, opts) {
|
|
5139
6167
|
if (!pid)
|
|
5140
6168
|
return;
|
|
@@ -5150,10 +6178,48 @@ function shellQuote(value) {
|
|
|
5150
6178
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5151
6179
|
}
|
|
5152
6180
|
function codewithProfileCandidateFromLine(line) {
|
|
5153
|
-
const
|
|
5154
|
-
if (
|
|
5155
|
-
return
|
|
5156
|
-
|
|
6181
|
+
const trimmed = line.trim();
|
|
6182
|
+
if (!trimmed || trimmed === "No auth profiles saved.")
|
|
6183
|
+
return;
|
|
6184
|
+
const cols = trimmed.split(/\s+/);
|
|
6185
|
+
const candidate = cols[0] === "*" ? cols[1] : cols[0];
|
|
6186
|
+
if (candidate === "NAME" && (cols[1] === "ACCOUNT" || cols[2] === "ACCOUNT"))
|
|
6187
|
+
return;
|
|
6188
|
+
if (candidate === '"name":') {
|
|
6189
|
+
const jsonName = trimmed.match(/"name"\s*:\s*"([^"]+)"/)?.[1];
|
|
6190
|
+
if (jsonName)
|
|
6191
|
+
return jsonName;
|
|
6192
|
+
}
|
|
6193
|
+
return candidate;
|
|
6194
|
+
}
|
|
6195
|
+
function codewithProfileCandidatesFromJson(value) {
|
|
6196
|
+
const candidates = [];
|
|
6197
|
+
const root = value && typeof value === "object" ? value : undefined;
|
|
6198
|
+
const addProfile = (entry) => {
|
|
6199
|
+
if (!entry || typeof entry !== "object")
|
|
6200
|
+
return;
|
|
6201
|
+
const name = entry.name;
|
|
6202
|
+
if (typeof name === "string" && name)
|
|
6203
|
+
candidates.push(name);
|
|
6204
|
+
};
|
|
6205
|
+
const data = root?.data;
|
|
6206
|
+
if (Array.isArray(data))
|
|
6207
|
+
data.forEach(addProfile);
|
|
6208
|
+
const profiles = root?.profiles;
|
|
6209
|
+
if (Array.isArray(profiles))
|
|
6210
|
+
profiles.forEach(addProfile);
|
|
6211
|
+
return candidates;
|
|
6212
|
+
}
|
|
6213
|
+
function codewithProfileCandidatesFromOutput(output) {
|
|
6214
|
+
const trimmed = output.trim();
|
|
6215
|
+
const jsonStart = trimmed.indexOf("{");
|
|
6216
|
+
const jsonEnd = trimmed.lastIndexOf("}");
|
|
6217
|
+
if (jsonStart >= 0 && jsonEnd >= jsonStart) {
|
|
6218
|
+
try {
|
|
6219
|
+
return codewithProfileCandidatesFromJson(JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1)));
|
|
6220
|
+
} catch {}
|
|
6221
|
+
}
|
|
6222
|
+
return output.split(/\r?\n/).map(codewithProfileCandidateFromLine).filter(Boolean);
|
|
5157
6223
|
}
|
|
5158
6224
|
function codewithProfileForError(profile) {
|
|
5159
6225
|
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
@@ -5246,6 +6312,7 @@ function commandSpec(target, opts) {
|
|
|
5246
6312
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
5247
6313
|
stdin: invocation.stdin,
|
|
5248
6314
|
allowlist: agentTarget.allowlist,
|
|
6315
|
+
agentProvider: agentTarget.provider,
|
|
5249
6316
|
worktree: agentTarget.worktree
|
|
5250
6317
|
};
|
|
5251
6318
|
}
|
|
@@ -5259,6 +6326,7 @@ function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
|
5259
6326
|
Object.assign(env, spec.env ?? {});
|
|
5260
6327
|
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
5261
6328
|
env.PATH = normalizeExecutionPath(env);
|
|
6329
|
+
env.SHLVL ||= "1";
|
|
5262
6330
|
Object.assign(env, metadataEnv(metadata));
|
|
5263
6331
|
return env;
|
|
5264
6332
|
}
|
|
@@ -5330,7 +6398,7 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
5330
6398
|
return [
|
|
5331
6399
|
"__openloops_prepare_worktree() {",
|
|
5332
6400
|
` local repo=${shellQuote(repoRoot)} path=${shellQuote(path)} branch=${shellQuote(branch)}`,
|
|
5333
|
-
" local top expected_common actual_common current",
|
|
6401
|
+
" local top expected_common actual_common current status recovered",
|
|
5334
6402
|
' if [ -L "$path" ]; then echo "refusing symlinked worktree path $path" >&2; return 1; fi',
|
|
5335
6403
|
' if [ -e "$path" ]; then',
|
|
5336
6404
|
' top="$(git -C "$path" rev-parse --show-toplevel 2>/dev/null)" || { echo "refusing to reuse non-worktree path: $path" >&2; return 1; }',
|
|
@@ -5339,7 +6407,19 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
5339
6407
|
' actual_common="$(cd "$path" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P)"',
|
|
5340
6408
|
' if [ -z "$expected_common" ] || [ "$expected_common" != "$actual_common" ]; then echo "existing worktree $path belongs to a different git common dir" >&2; return 1; fi',
|
|
5341
6409
|
' current="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
5342
|
-
' if [ "$current" != "$branch" ]; then
|
|
6410
|
+
' if [ "$current" != "$branch" ]; then',
|
|
6411
|
+
' 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; }',
|
|
6412
|
+
' 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',
|
|
6413
|
+
' if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch"; then',
|
|
6414
|
+
' 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; }',
|
|
6415
|
+
' elif [ -z "$current" ]; then',
|
|
6416
|
+
' 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; }',
|
|
6417
|
+
" else",
|
|
6418
|
+
' 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',
|
|
6419
|
+
" fi",
|
|
6420
|
+
' recovered="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
6421
|
+
' 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',
|
|
6422
|
+
" fi",
|
|
5343
6423
|
" return 0",
|
|
5344
6424
|
" fi",
|
|
5345
6425
|
' 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; }',
|
|
@@ -5405,7 +6485,7 @@ function remotePreflightScript(spec, metadata) {
|
|
|
5405
6485
|
}
|
|
5406
6486
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
5407
6487
|
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
5408
|
-
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE",
|
|
6488
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", "__openloops_codewith_profile_list_contains() {", ` printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk '{ line = $0; gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); if (line == "" || line == "No auth profiles saved.") next; if (line ~ /"(data|profiles)"[[:space:]]*:[[:space:]]*\\[/) { in_profiles = 1; next } if (in_profiles && line ~ /^\\]/) { in_profiles = 0; next } json = line; if (in_profiles && json ~ /"name"[[:space:]]*:[[:space:]]*"/) { sub(/^.*"name"[[:space:]]*:[[:space:]]*"/, "", json); sub(/".*$/, "", json); if (json == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1; next } split(line, cols, /[[:space:]]+/); candidate = (cols[1] == "*" ? cols[2] : cols[1]); if (candidate == "NAME" && (cols[2] == "ACCOUNT" || cols[3] == "ACCOUNT")) next; if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'`, "}", `if __OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list --json 2>/dev/null)" && __openloops_codewith_profile_list_contains; then`, " :", "else", ` __OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", " }", " if ! __openloops_codewith_profile_list_contains; then", ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", " fi", "fi");
|
|
5409
6489
|
}
|
|
5410
6490
|
return lines.join(`
|
|
5411
6491
|
`);
|
|
@@ -5420,9 +6500,28 @@ function transportEnv(opts) {
|
|
|
5420
6500
|
env[key] = value;
|
|
5421
6501
|
}
|
|
5422
6502
|
env.PATH = normalizeExecutionPath(env);
|
|
6503
|
+
env.SHLVL ||= "1";
|
|
5423
6504
|
return env;
|
|
5424
6505
|
}
|
|
6506
|
+
function remotePreflightFailureMessage(machineId, detail) {
|
|
6507
|
+
const normalized = detail.trim();
|
|
6508
|
+
if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|Offending .*key in .*known_hosts/i.test(normalized)) {
|
|
6509
|
+
return [
|
|
6510
|
+
`remote preflight failed on ${machineId}: SSH host key verification failed.`,
|
|
6511
|
+
`Verify ${machineId}'s host identity and repair SSH known_hosts/trust material outside OpenLoops;`,
|
|
6512
|
+
"OpenLoops will not disable host-key checking or modify known_hosts automatically.",
|
|
6513
|
+
normalized ? `Transport detail: ${normalized}` : ""
|
|
6514
|
+
].filter(Boolean).join(" ");
|
|
6515
|
+
}
|
|
6516
|
+
return `remote preflight failed on ${machineId}${normalized ? `: ${normalized}` : ""}`;
|
|
6517
|
+
}
|
|
5425
6518
|
function assertCodewithProfileListed(profile, result) {
|
|
6519
|
+
const profiles = codewithProfileSetFromResult(result);
|
|
6520
|
+
if (!profiles.has(profile)) {
|
|
6521
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
function codewithProfileSetFromResult(result) {
|
|
5426
6525
|
if (result.error) {
|
|
5427
6526
|
throw new Error(`codewith auth profile preflight failed: ${result.error}`);
|
|
5428
6527
|
}
|
|
@@ -5430,32 +6529,45 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
5430
6529
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5431
6530
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
5432
6531
|
}
|
|
5433
|
-
|
|
5434
|
-
if (!profiles.has(profile)) {
|
|
5435
|
-
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
5436
|
-
}
|
|
6532
|
+
return new Set(codewithProfileCandidatesFromOutput(result.stdout || ""));
|
|
5437
6533
|
}
|
|
5438
6534
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
5439
6535
|
if (spec.nativeAuthProfile?.provider !== "codewith")
|
|
5440
6536
|
return;
|
|
5441
|
-
const
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
6537
|
+
const runProfileList = (args) => {
|
|
6538
|
+
const result = spawnSync4(spec.command, args, {
|
|
6539
|
+
encoding: "utf8",
|
|
6540
|
+
env,
|
|
6541
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6542
|
+
timeout: 15000
|
|
6543
|
+
});
|
|
6544
|
+
return {
|
|
6545
|
+
status: result.status,
|
|
6546
|
+
stdout: result.stdout ?? "",
|
|
6547
|
+
stderr: result.stderr ?? "",
|
|
6548
|
+
error: result.error?.message
|
|
6549
|
+
};
|
|
6550
|
+
};
|
|
6551
|
+
const jsonResult = runProfileList(["profile", "list", "--json"]);
|
|
6552
|
+
try {
|
|
6553
|
+
if (codewithProfileSetFromResult(jsonResult).has(spec.nativeAuthProfile.profile))
|
|
6554
|
+
return;
|
|
6555
|
+
} catch {}
|
|
6556
|
+
assertCodewithProfileListed(spec.nativeAuthProfile.profile, runProfileList(["profile", "list"]));
|
|
5453
6557
|
}
|
|
5454
6558
|
async function preflightNativeAuthProfile(spec, env) {
|
|
5455
6559
|
if (spec.nativeAuthProfile?.provider !== "codewith")
|
|
5456
6560
|
return;
|
|
5457
|
-
const
|
|
5458
|
-
|
|
6561
|
+
const jsonResult = await spawnCapture(spec.command, ["profile", "list", "--json"], { env, timeoutMs: 15000 });
|
|
6562
|
+
try {
|
|
6563
|
+
if (codewithProfileSetFromResult(jsonResult).has(spec.nativeAuthProfile.profile))
|
|
6564
|
+
return;
|
|
6565
|
+
} catch {}
|
|
6566
|
+
const tableResult = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
6567
|
+
assertCodewithProfileListed(spec.nativeAuthProfile.profile, tableResult);
|
|
6568
|
+
}
|
|
6569
|
+
function spawnDetail(result) {
|
|
6570
|
+
return (result.stderr || result.stdout || result.error || "").toString().trim();
|
|
5459
6571
|
}
|
|
5460
6572
|
function resolvedDirEquals(left, right) {
|
|
5461
6573
|
try {
|
|
@@ -5506,8 +6618,45 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
5506
6618
|
}
|
|
5507
6619
|
const current = await git(["-C", path, "branch", "--show-current"]);
|
|
5508
6620
|
const actualBranch = current.stdout.trim();
|
|
5509
|
-
if (current.error || (current.status ?? 1) !== 0
|
|
5510
|
-
return { error: `existing worktree ${path}
|
|
6621
|
+
if (current.error || (current.status ?? 1) !== 0) {
|
|
6622
|
+
return { error: `existing worktree ${path} branch could not be inspected${spawnDetail(current) ? `: ${spawnDetail(current)}` : ""}` };
|
|
6623
|
+
}
|
|
6624
|
+
if (actualBranch !== branch) {
|
|
6625
|
+
const actualLabel = actualBranch || "unknown";
|
|
6626
|
+
const status = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
|
|
6627
|
+
if (status.error || (status.status ?? 1) !== 0) {
|
|
6628
|
+
const detail = spawnDetail(status);
|
|
6629
|
+
return {
|
|
6630
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and cleanliness could not be checked; inspect or remove the stale worktree${detail ? `: ${detail}` : ""}`
|
|
6631
|
+
};
|
|
6632
|
+
}
|
|
6633
|
+
if (status.stdout.trim()) {
|
|
6634
|
+
return {
|
|
6635
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and has local changes; commit/stash them or remove the stale worktree before retrying`
|
|
6636
|
+
};
|
|
6637
|
+
}
|
|
6638
|
+
const hasBranch2 = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
6639
|
+
const checkout = hasBranch2.status === 0 ? await git(["-C", path, "checkout", branch]) : actualBranch ? {
|
|
6640
|
+
status: 1,
|
|
6641
|
+
stdout: "",
|
|
6642
|
+
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`,
|
|
6643
|
+
signal: null,
|
|
6644
|
+
timedOut: false
|
|
6645
|
+
} : await git(["-C", path, "checkout", "-b", branch]);
|
|
6646
|
+
if (checkout.error || (checkout.status ?? 1) !== 0) {
|
|
6647
|
+
const detail = spawnDetail(checkout);
|
|
6648
|
+
return {
|
|
6649
|
+
error: `existing worktree ${path} is clean but could not recover branch ${branch} from ${actualLabel}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
6650
|
+
};
|
|
6651
|
+
}
|
|
6652
|
+
const recovered = await git(["-C", path, "branch", "--show-current"]);
|
|
6653
|
+
if (recovered.error || (recovered.status ?? 1) !== 0 || recovered.stdout.trim() !== branch) {
|
|
6654
|
+
const recoveredBranch = recovered.stdout.trim() || "unknown";
|
|
6655
|
+
const detail = spawnDetail(recovered);
|
|
6656
|
+
return {
|
|
6657
|
+
error: `existing worktree ${path} branch recovery ended on ${recoveredBranch}, expected ${branch}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
6658
|
+
};
|
|
6659
|
+
}
|
|
5511
6660
|
}
|
|
5512
6661
|
return { cwd: worktree.cwd };
|
|
5513
6662
|
}
|
|
@@ -5516,14 +6665,14 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
5516
6665
|
return { error: `worktree repoRoot is not a git repository: ${repoRoot}` };
|
|
5517
6666
|
}
|
|
5518
6667
|
try {
|
|
5519
|
-
|
|
6668
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
5520
6669
|
} catch (error) {
|
|
5521
6670
|
return { error: `could not create worktree parent directory: ${error instanceof Error ? error.message : String(error)}` };
|
|
5522
6671
|
}
|
|
5523
6672
|
const hasBranch = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
5524
6673
|
const add = hasBranch.status === 0 ? await git(["-C", repoRoot, "worktree", "add", path, branch]) : await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
5525
6674
|
if (add.error || (add.status ?? 1) !== 0) {
|
|
5526
|
-
const detail = (add
|
|
6675
|
+
const detail = spawnDetail(add);
|
|
5527
6676
|
return { error: `git worktree add failed for ${path}${detail ? `: ${detail}` : ""}` };
|
|
5528
6677
|
}
|
|
5529
6678
|
return { cwd: worktree.cwd };
|
|
@@ -5558,7 +6707,7 @@ function worktreeFallbackSpec(target, opts, fallbackCwd) {
|
|
|
5558
6707
|
}
|
|
5559
6708
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
5560
6709
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
5561
|
-
const result =
|
|
6710
|
+
const result = spawnSync4(plan.command, plan.args, {
|
|
5562
6711
|
encoding: "utf8",
|
|
5563
6712
|
env: transportEnv(opts),
|
|
5564
6713
|
input: remotePreflightScript(spec, metadata),
|
|
@@ -5569,7 +6718,7 @@ function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
|
5569
6718
|
throw new Error(`remote preflight failed on ${machine.id}: ${result.error.message}`);
|
|
5570
6719
|
if ((result.status ?? 1) !== 0) {
|
|
5571
6720
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5572
|
-
throw new Error(
|
|
6721
|
+
throw new Error(remotePreflightFailureMessage(machine.id, detail));
|
|
5573
6722
|
}
|
|
5574
6723
|
}
|
|
5575
6724
|
async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
@@ -5657,6 +6806,9 @@ async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
|
5657
6806
|
if (timedOut || idleTimedOut) {
|
|
5658
6807
|
return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
|
|
5659
6808
|
}
|
|
6809
|
+
if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
|
|
6810
|
+
return successResult(startedAt, fields);
|
|
6811
|
+
}
|
|
5660
6812
|
if (error || exitCode !== 0) {
|
|
5661
6813
|
return failureResult(startedAt, error ?? `remote process on ${machine.id} exited with code ${exitCode ?? "unknown"}`, fields);
|
|
5662
6814
|
}
|
|
@@ -5792,6 +6944,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
5792
6944
|
if (timedOut || idleTimedOut) {
|
|
5793
6945
|
return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
|
|
5794
6946
|
}
|
|
6947
|
+
if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
|
|
6948
|
+
return successResult(startedAt, fields);
|
|
6949
|
+
}
|
|
5795
6950
|
if (error || exitCode !== 0) {
|
|
5796
6951
|
return failureResult(startedAt, error ?? `process exited with code ${exitCode ?? "unknown"}`, fields);
|
|
5797
6952
|
}
|
|
@@ -5907,55 +7062,6 @@ ${evidence.join(`
|
|
|
5907
7062
|
import { generateObject } from "ai";
|
|
5908
7063
|
import { z } from "zod";
|
|
5909
7064
|
|
|
5910
|
-
// src/lib/run-envelope.ts
|
|
5911
|
-
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
5912
|
-
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
5913
|
-
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
5914
|
-
if (!text)
|
|
5915
|
-
return;
|
|
5916
|
-
const scrubbed = scrubSecrets(text);
|
|
5917
|
-
if (scrubbed.length <= limit * 2)
|
|
5918
|
-
return scrubbed;
|
|
5919
|
-
const omitted = scrubbed.length - limit * 2;
|
|
5920
|
-
return `${scrubbed.slice(0, limit)}
|
|
5921
|
-
[... ${omitted} chars omitted ...]
|
|
5922
|
-
${scrubbed.slice(-limit)}`;
|
|
5923
|
-
}
|
|
5924
|
-
function summarizeOutput(stdout, stderr) {
|
|
5925
|
-
return {
|
|
5926
|
-
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
5927
|
-
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
5928
|
-
stdoutExcerpt: boundedExcerpt(stdout),
|
|
5929
|
-
stderrExcerpt: boundedExcerpt(stderr)
|
|
5930
|
-
};
|
|
5931
|
-
}
|
|
5932
|
-
function summarizeExecutorResult(result) {
|
|
5933
|
-
return {
|
|
5934
|
-
...summarizeOutput(result.stdout, result.stderr),
|
|
5935
|
-
status: result.status,
|
|
5936
|
-
exitCode: result.exitCode,
|
|
5937
|
-
durationMs: result.durationMs,
|
|
5938
|
-
error: boundedExcerpt(result.error)
|
|
5939
|
-
};
|
|
5940
|
-
}
|
|
5941
|
-
function isBlockedStepRun(step) {
|
|
5942
|
-
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
5943
|
-
}
|
|
5944
|
-
function summarizeWorkflowStepRun(step) {
|
|
5945
|
-
return {
|
|
5946
|
-
...summarizeOutput(step.stdout, step.stderr),
|
|
5947
|
-
stepId: step.stepId,
|
|
5948
|
-
status: step.status,
|
|
5949
|
-
exitCode: step.exitCode,
|
|
5950
|
-
durationMs: step.durationMs,
|
|
5951
|
-
error: boundedExcerpt(step.error),
|
|
5952
|
-
blocked: isBlockedStepRun(step)
|
|
5953
|
-
};
|
|
5954
|
-
}
|
|
5955
|
-
function workflowRunEnvelope(run, steps) {
|
|
5956
|
-
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
5957
|
-
}
|
|
5958
|
-
|
|
5959
7065
|
// src/lib/goal/model-factory.ts
|
|
5960
7066
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
5961
7067
|
var DEFAULT_GOAL_MODEL = "openai/gpt-4o-mini";
|
|
@@ -6820,7 +7926,7 @@ var MAX_RETRY_EXPONENT = 20;
|
|
|
6820
7926
|
function retryBackoffDelayMs(loop, run, random = Math.random) {
|
|
6821
7927
|
const attempt = Math.max(1, run.attempt);
|
|
6822
7928
|
const failure = classifyRunFailure(run);
|
|
6823
|
-
const throttled = failure?.classification === "rate_limit" || failure?.classification === "auth";
|
|
7929
|
+
const throttled = failure?.classification === "rate_limit" || failure?.classification === "auth" || failure?.classification === "provider_unavailable";
|
|
6824
7930
|
const growth = 2 ** Math.min(attempt - 1, MAX_RETRY_EXPONENT);
|
|
6825
7931
|
const base = loop.retryDelayMs * growth * (throttled ? THROTTLED_RETRY_MULTIPLIER : 1);
|
|
6826
7932
|
const jitter = 0.5 + random();
|
|
@@ -6947,20 +8053,21 @@ async function executeClaimedRun(deps) {
|
|
|
6947
8053
|
daemonLeaseId: deps.daemonLeaseId,
|
|
6948
8054
|
onSpawn: (pid) => deps.store.markRunPid(run.id, pid, deps.runnerId, { daemonLeaseId: deps.daemonLeaseId })
|
|
6949
8055
|
})))(deps.loop, deps.run);
|
|
8056
|
+
const finalResult = deps.finalizeResult?.(result, deps.loop, deps.run) ?? result;
|
|
6950
8057
|
deps.beforeFinalize?.(deps.loop, deps.run);
|
|
6951
8058
|
return deps.store.finalizeRun(deps.run.id, {
|
|
6952
|
-
status:
|
|
6953
|
-
finishedAt:
|
|
6954
|
-
durationMs:
|
|
6955
|
-
stdout:
|
|
6956
|
-
stderr:
|
|
6957
|
-
exitCode:
|
|
6958
|
-
error:
|
|
6959
|
-
pid:
|
|
8059
|
+
status: finalResult.status,
|
|
8060
|
+
finishedAt: finalResult.finishedAt,
|
|
8061
|
+
durationMs: finalResult.durationMs,
|
|
8062
|
+
stdout: finalResult.stdout,
|
|
8063
|
+
stderr: finalResult.stderr,
|
|
8064
|
+
exitCode: finalResult.exitCode,
|
|
8065
|
+
error: finalResult.error,
|
|
8066
|
+
pid: finalResult.pid
|
|
6960
8067
|
}, {
|
|
6961
8068
|
claimedBy: deps.runnerId,
|
|
6962
8069
|
daemonLeaseId: deps.daemonLeaseId,
|
|
6963
|
-
now: deps.now?.() ?? new Date(
|
|
8070
|
+
now: deps.now?.() ?? new Date(finalResult.finishedAt)
|
|
6964
8071
|
});
|
|
6965
8072
|
} catch (err) {
|
|
6966
8073
|
deps.onError?.(deps.loop, err);
|
|
@@ -7195,10 +8302,10 @@ async function tick(deps) {
|
|
|
7195
8302
|
}
|
|
7196
8303
|
|
|
7197
8304
|
// src/daemon/control.ts
|
|
7198
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
7199
|
-
import { spawnSync as
|
|
7200
|
-
import { hostname } from "os";
|
|
7201
|
-
import { dirname as dirname4, join as
|
|
8305
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
8306
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
8307
|
+
import { hostname as hostname2 } from "os";
|
|
8308
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
7202
8309
|
|
|
7203
8310
|
// src/daemon/loop.ts
|
|
7204
8311
|
function realSleep(ms) {
|
|
@@ -7228,7 +8335,7 @@ function readPidRecord(path = pidFilePath()) {
|
|
|
7228
8335
|
return;
|
|
7229
8336
|
let raw;
|
|
7230
8337
|
try {
|
|
7231
|
-
raw =
|
|
8338
|
+
raw = readFileSync5(path, "utf8").trim();
|
|
7232
8339
|
} catch {
|
|
7233
8340
|
return;
|
|
7234
8341
|
}
|
|
@@ -7250,11 +8357,11 @@ function readPidRecord(path = pidFilePath()) {
|
|
|
7250
8357
|
return Number.isInteger(pid) && pid > 0 ? { pid } : undefined;
|
|
7251
8358
|
}
|
|
7252
8359
|
function writePid(pid = process.pid, path = pidFilePath()) {
|
|
7253
|
-
|
|
7254
|
-
|
|
8360
|
+
mkdirSync6(dirname4(path), { recursive: true, mode: 448 });
|
|
8361
|
+
writeFileSync3(path, JSON.stringify({ pid, startedAt: processStartTimeMs(pid) }));
|
|
7255
8362
|
}
|
|
7256
8363
|
function removePid(path = pidFilePath()) {
|
|
7257
|
-
|
|
8364
|
+
rmSync4(path, { force: true });
|
|
7258
8365
|
}
|
|
7259
8366
|
function isAlive(pid, startedAt) {
|
|
7260
8367
|
try {
|
|
@@ -7284,7 +8391,7 @@ function ownProcessGroupId() {
|
|
|
7284
8391
|
return pgrp;
|
|
7285
8392
|
}
|
|
7286
8393
|
try {
|
|
7287
|
-
const run =
|
|
8394
|
+
const run = spawnSync5("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8" });
|
|
7288
8395
|
const pgid = Number(run.stdout.trim());
|
|
7289
8396
|
if (run.status === 0 && Number.isInteger(pgid) && pgid > 0)
|
|
7290
8397
|
return pgid;
|
|
@@ -7350,7 +8457,7 @@ function daemonStatus(store, path = pidFilePath()) {
|
|
|
7350
8457
|
return {
|
|
7351
8458
|
...isDaemonRunning(path),
|
|
7352
8459
|
lease: store.getDaemonLease(),
|
|
7353
|
-
host:
|
|
8460
|
+
host: hostname2(),
|
|
7354
8461
|
loops: {
|
|
7355
8462
|
total: store.countLoops(),
|
|
7356
8463
|
active: store.countLoops("active"),
|
|
@@ -7380,7 +8487,7 @@ async function stopDaemon(opts = {}) {
|
|
|
7380
8487
|
if (!state.running || !state.pid)
|
|
7381
8488
|
return { wasRunning: false, stopped: false, forced: false };
|
|
7382
8489
|
const sleep = opts.sleep ?? realSleep;
|
|
7383
|
-
const store = new Store(
|
|
8490
|
+
const store = new Store(join7(dirname4(path), "loops.db"));
|
|
7384
8491
|
try {
|
|
7385
8492
|
const lease = store.getDaemonLease();
|
|
7386
8493
|
const leaseVerified = Boolean(lease && lease.pid === state.pid && new Date(lease.expiresAt).getTime() > Date.now());
|
|
@@ -7457,7 +8564,7 @@ function rotateDaemonLog(path = daemonLogPath(), maxBytes = DAEMON_LOG_MAX_BYTES
|
|
|
7457
8564
|
if (size < maxBytes)
|
|
7458
8565
|
return false;
|
|
7459
8566
|
try {
|
|
7460
|
-
|
|
8567
|
+
rmSync5(`${path}.${keep}`, { force: true });
|
|
7461
8568
|
for (let i = keep - 1;i >= 1; i--) {
|
|
7462
8569
|
if (existsSync5(`${path}.${i}`))
|
|
7463
8570
|
renameSync2(`${path}.${i}`, `${path}.${i + 1}`);
|
|
@@ -7484,7 +8591,7 @@ async function runDaemon(opts = {}) {
|
|
|
7484
8591
|
const ownStore = !opts.store;
|
|
7485
8592
|
const store = opts.store ?? new Store;
|
|
7486
8593
|
const leaseId = genId();
|
|
7487
|
-
const runnerId = `${
|
|
8594
|
+
const runnerId = `${hostname3()}:${process.pid}:${leaseId}`;
|
|
7488
8595
|
const intervalMs = opts.intervalMs ?? intervalFromEnv() ?? 1000;
|
|
7489
8596
|
const leaseTtlMs = opts.leaseTtlMs ?? Math.max(60000, intervalMs * 10);
|
|
7490
8597
|
const laneConcurrency = resolveLaneConcurrency(opts);
|
|
@@ -7493,7 +8600,7 @@ async function runDaemon(opts = {}) {
|
|
|
7493
8600
|
const lease = store.acquireDaemonLease({
|
|
7494
8601
|
id: leaseId,
|
|
7495
8602
|
pid: process.pid,
|
|
7496
|
-
hostname:
|
|
8603
|
+
hostname: hostname3(),
|
|
7497
8604
|
ttlMs: leaseTtlMs
|
|
7498
8605
|
});
|
|
7499
8606
|
if (!lease)
|
|
@@ -7506,6 +8613,7 @@ async function runDaemon(opts = {}) {
|
|
|
7506
8613
|
let runAbort = new AbortController;
|
|
7507
8614
|
const activeRuns = new Map;
|
|
7508
8615
|
const activeByLane = { command: 0, agent: 0 };
|
|
8616
|
+
const isControlledStopInterruption = (result) => stopFlag && !leaseLost && result.status === "failed" && result.error?.includes("terminated by SIGTERM") === true;
|
|
7509
8617
|
const requestStop = (message) => {
|
|
7510
8618
|
stopFlag = true;
|
|
7511
8619
|
if (!runAbort.signal.aborted)
|
|
@@ -7527,7 +8635,7 @@ async function runDaemon(opts = {}) {
|
|
|
7527
8635
|
reacquired = store.acquireDaemonLease({
|
|
7528
8636
|
id: leaseId,
|
|
7529
8637
|
pid: process.pid,
|
|
7530
|
-
hostname:
|
|
8638
|
+
hostname: hostname3(),
|
|
7531
8639
|
ttlMs: leaseTtlMs
|
|
7532
8640
|
});
|
|
7533
8641
|
} catch {
|
|
@@ -7589,6 +8697,16 @@ async function runDaemon(opts = {}) {
|
|
|
7589
8697
|
store.recordRunProcess(run.id, info, { daemonLeaseId: leaseId });
|
|
7590
8698
|
}
|
|
7591
8699
|
})),
|
|
8700
|
+
finalizeResult: (result) => {
|
|
8701
|
+
if (!isControlledStopInterruption(result))
|
|
8702
|
+
return result;
|
|
8703
|
+
return {
|
|
8704
|
+
...result,
|
|
8705
|
+
status: "skipped",
|
|
8706
|
+
exitCode: undefined,
|
|
8707
|
+
error: `${RESTART_INTERRUPTED_RUN_PREFIX}: child process terminated by SIGTERM during daemon stop/restart`
|
|
8708
|
+
};
|
|
8709
|
+
},
|
|
7592
8710
|
onError: (loop, err) => log(`loop ${loop.id} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
7593
8711
|
});
|
|
7594
8712
|
ensureLease();
|
|
@@ -7682,7 +8800,7 @@ async function startDaemon(opts) {
|
|
|
7682
8800
|
return { started: false, alreadyRunning: true, pid: state.pid };
|
|
7683
8801
|
if (state.stale)
|
|
7684
8802
|
removePid();
|
|
7685
|
-
const out =
|
|
8803
|
+
const out = openSync2(daemonLogPath(), "a");
|
|
7686
8804
|
const child = spawn3(opts.execPath ?? process.execPath, [opts.cliEntry, ...opts.args ?? ["daemon", "run"]], {
|
|
7687
8805
|
detached: true,
|
|
7688
8806
|
stdio: ["ignore", out, out]
|
|
@@ -7700,8 +8818,8 @@ async function startDaemon(opts) {
|
|
|
7700
8818
|
}
|
|
7701
8819
|
|
|
7702
8820
|
// src/daemon/install.ts
|
|
7703
|
-
import { chmodSync as chmodSync2, mkdirSync as
|
|
7704
|
-
import { spawnSync as
|
|
8821
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
8822
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
7705
8823
|
import { dirname as dirname5 } from "path";
|
|
7706
8824
|
function systemdEscapeExecPart(part) {
|
|
7707
8825
|
const escaped = part.replaceAll("%", "%%");
|
|
@@ -7733,9 +8851,9 @@ function installStartup(cliEntry, execPath = process.execPath, args = ["daemon",
|
|
|
7733
8851
|
const dataDirPath = ensureDataDir();
|
|
7734
8852
|
if (platform === "linux") {
|
|
7735
8853
|
const path = systemdServicePath();
|
|
7736
|
-
|
|
8854
|
+
mkdirSync7(dirname5(path), { recursive: true, mode: 448 });
|
|
7737
8855
|
const execStart = [execPath, cliEntry, ...args].map(systemdEscapeExecPart).join(" ");
|
|
7738
|
-
|
|
8856
|
+
writeFileSync4(path, `[Unit]
|
|
7739
8857
|
Description=Hasna OpenLoops daemon
|
|
7740
8858
|
After=basic.target
|
|
7741
8859
|
|
|
@@ -7763,8 +8881,8 @@ WantedBy=default.target
|
|
|
7763
8881
|
}
|
|
7764
8882
|
if (platform === "darwin") {
|
|
7765
8883
|
const path = launchdPlistPath();
|
|
7766
|
-
|
|
7767
|
-
|
|
8884
|
+
mkdirSync7(dirname5(path), { recursive: true, mode: 448 });
|
|
8885
|
+
writeFileSync4(path, `<?xml version="1.0" encoding="UTF-8"?>
|
|
7768
8886
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7769
8887
|
<plist version="1.0">
|
|
7770
8888
|
<dict>
|
|
@@ -7801,7 +8919,7 @@ ${args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join(`
|
|
|
7801
8919
|
function enableStartup(result) {
|
|
7802
8920
|
const commands = result.platform === "linux" ? ["systemctl --user daemon-reload", "systemctl --user enable --now loops-daemon.service"] : result.platform === "darwin" ? launchctlCommands(result.path) : [];
|
|
7803
8921
|
return commands.map((command) => {
|
|
7804
|
-
const run =
|
|
8922
|
+
const run = spawnSync6("sh", ["-c", command], {
|
|
7805
8923
|
encoding: "utf8",
|
|
7806
8924
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7807
8925
|
});
|
|
@@ -7816,7 +8934,7 @@ function enableStartup(result) {
|
|
|
7816
8934
|
// package.json
|
|
7817
8935
|
var package_default = {
|
|
7818
8936
|
name: "@hasna/loops",
|
|
7819
|
-
version: "0.4.
|
|
8937
|
+
version: "0.4.22",
|
|
7820
8938
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7821
8939
|
type: "module",
|
|
7822
8940
|
main: "dist/index.js",
|
|
@@ -7825,6 +8943,7 @@ var package_default = {
|
|
|
7825
8943
|
loops: "dist/cli/index.js",
|
|
7826
8944
|
"loops-daemon": "dist/daemon/index.js",
|
|
7827
8945
|
"loops-api": "dist/api/index.js",
|
|
8946
|
+
"loops-serve": "dist/serve/index.js",
|
|
7828
8947
|
"loops-runner": "dist/runner/index.js",
|
|
7829
8948
|
"loops-mcp": "dist/mcp/index.js"
|
|
7830
8949
|
},
|
|
@@ -7837,6 +8956,14 @@ var package_default = {
|
|
|
7837
8956
|
types: "./dist/sdk/index.d.ts",
|
|
7838
8957
|
import: "./dist/sdk/index.js"
|
|
7839
8958
|
},
|
|
8959
|
+
"./sdk/http": {
|
|
8960
|
+
types: "./dist/sdk/http.d.ts",
|
|
8961
|
+
import: "./dist/sdk/http.js"
|
|
8962
|
+
},
|
|
8963
|
+
"./serve": {
|
|
8964
|
+
types: "./dist/serve/index.d.ts",
|
|
8965
|
+
import: "./dist/serve/index.js"
|
|
8966
|
+
},
|
|
7840
8967
|
"./mcp": {
|
|
7841
8968
|
types: "./dist/mcp/index.d.ts",
|
|
7842
8969
|
import: "./dist/mcp/index.js"
|
|
@@ -7882,7 +9009,7 @@ var package_default = {
|
|
|
7882
9009
|
"LICENSE"
|
|
7883
9010
|
],
|
|
7884
9011
|
scripts: {
|
|
7885
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
9012
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
7886
9013
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
7887
9014
|
typecheck: "tsc --noEmit",
|
|
7888
9015
|
test: "bun test",
|
|
@@ -7922,13 +9049,16 @@ var package_default = {
|
|
|
7922
9049
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
7923
9050
|
ai: "6.0.204",
|
|
7924
9051
|
commander: "^13.1.0",
|
|
9052
|
+
pg: "^8.13.1",
|
|
7925
9053
|
zod: "4.4.3"
|
|
7926
9054
|
},
|
|
7927
9055
|
optionalDependencies: {
|
|
7928
|
-
"@hasna/machines": "0.0.49"
|
|
9056
|
+
"@hasna/machines": "0.0.49",
|
|
9057
|
+
"@hasna/contracts": "^0.4.2"
|
|
7929
9058
|
},
|
|
7930
9059
|
devDependencies: {
|
|
7931
9060
|
"@types/bun": "latest",
|
|
9061
|
+
"@types/pg": "^8.11.10",
|
|
7932
9062
|
typescript: "^5.7.3"
|
|
7933
9063
|
},
|
|
7934
9064
|
publishConfig: {
|