@hasna/loops 0.3.53 → 0.3.55
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/README.md +77 -4
- package/dist/cli/index.js +477 -108
- package/dist/daemon/index.js +223 -29
- package/dist/index.js +308 -75
- package/dist/lib/store.d.ts +16 -1
- package/dist/lib/store.js +202 -11
- package/dist/lib/templates.d.ts +4 -2
- package/dist/lib/workflow-spec.d.ts +8 -2
- package/dist/sdk/index.js +219 -25
- package/dist/types.d.ts +24 -8
- package/docs/USAGE.md +82 -4
- package/docs/workflows/transcript-feedback-to-loops.json +2 -2
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -314,6 +314,8 @@ function updateReadyFlags(nodes, planStatus) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// src/lib/workflow-spec.ts
|
|
317
|
+
import { readFileSync } from "fs";
|
|
318
|
+
import { isAbsolute, resolve } from "path";
|
|
317
319
|
function assertObject(value, label) {
|
|
318
320
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
319
321
|
throw new Error(`${label} must be an object`);
|
|
@@ -329,6 +331,15 @@ function optionalPositiveInteger(value, label) {
|
|
|
329
331
|
throw new Error(`${label} must be a positive integer`);
|
|
330
332
|
return value;
|
|
331
333
|
}
|
|
334
|
+
function optionalTimeoutMs(value, label) {
|
|
335
|
+
if (value === undefined)
|
|
336
|
+
return;
|
|
337
|
+
if (value === null)
|
|
338
|
+
return null;
|
|
339
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
340
|
+
throw new Error(`${label} must be a positive integer or null for unlimited`);
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
332
343
|
function optionalBoolean(value, label) {
|
|
333
344
|
if (value === undefined)
|
|
334
345
|
return;
|
|
@@ -359,6 +370,38 @@ function optionalAccountRef(value, label) {
|
|
|
359
370
|
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
360
371
|
};
|
|
361
372
|
}
|
|
373
|
+
function promptFilePath(rawPath, opts) {
|
|
374
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
|
|
375
|
+
}
|
|
376
|
+
function readPromptFile(rawPath, label, opts) {
|
|
377
|
+
assertString(rawPath, `${label}.promptFile`);
|
|
378
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
379
|
+
let prompt;
|
|
380
|
+
try {
|
|
381
|
+
prompt = readFileSync(path, "utf8");
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
384
|
+
throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
|
|
385
|
+
}
|
|
386
|
+
if (prompt.trim() === "")
|
|
387
|
+
throw new Error(`${label}.promptFile must contain a non-empty prompt`);
|
|
388
|
+
return { prompt, promptSource: { type: "file", path } };
|
|
389
|
+
}
|
|
390
|
+
function matchingPromptSource(value, prompt, opts) {
|
|
391
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
392
|
+
return;
|
|
393
|
+
if (value.type !== "file")
|
|
394
|
+
return;
|
|
395
|
+
const rawPath = value.path;
|
|
396
|
+
if (typeof rawPath !== "string" || rawPath.trim() === "")
|
|
397
|
+
return;
|
|
398
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
399
|
+
try {
|
|
400
|
+
return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
362
405
|
function normalizeGoalSpec(value, label = "goal") {
|
|
363
406
|
if (value === undefined)
|
|
364
407
|
return;
|
|
@@ -381,11 +424,11 @@ function normalizeGoalSpec(value, label = "goal") {
|
|
|
381
424
|
autoExecute
|
|
382
425
|
};
|
|
383
426
|
}
|
|
384
|
-
function validateTarget(value, label) {
|
|
427
|
+
function validateTarget(value, label, opts) {
|
|
385
428
|
assertObject(value, label);
|
|
386
429
|
if (value.type === "command") {
|
|
387
430
|
assertString(value.command, `${label}.command`);
|
|
388
|
-
|
|
431
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
389
432
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
390
433
|
if (value.shell !== true && /\s/.test(value.command.trim())) {
|
|
391
434
|
throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
|
|
@@ -394,11 +437,17 @@ function validateTarget(value, label) {
|
|
|
394
437
|
}
|
|
395
438
|
if (value.type === "agent") {
|
|
396
439
|
assertString(value.provider, `${label}.provider`);
|
|
397
|
-
|
|
440
|
+
const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
|
|
441
|
+
const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
|
|
442
|
+
if (hasPrompt && hasPromptFile)
|
|
443
|
+
throw new Error(`${label} must use either prompt or promptFile, not both`);
|
|
444
|
+
if (!hasPrompt && !hasPromptFile)
|
|
445
|
+
throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
|
|
446
|
+
const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
|
|
398
447
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
399
448
|
if (!providers.includes(value.provider))
|
|
400
449
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
401
|
-
|
|
450
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
402
451
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
403
452
|
if (value.authProfile !== undefined) {
|
|
404
453
|
assertString(value.authProfile, `${label}.authProfile`);
|
|
@@ -495,11 +544,14 @@ function validateTarget(value, label) {
|
|
|
495
544
|
if (value.routing.eventSource !== undefined)
|
|
496
545
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
497
546
|
}
|
|
498
|
-
|
|
547
|
+
const target = { ...value };
|
|
548
|
+
delete target.promptFile;
|
|
549
|
+
delete target.promptSource;
|
|
550
|
+
return { ...target, ...promptFields };
|
|
499
551
|
}
|
|
500
552
|
throw new Error(`${label}.type must be command or agent`);
|
|
501
553
|
}
|
|
502
|
-
function normalizeCreateWorkflowInput(input) {
|
|
554
|
+
function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
503
555
|
assertString(input.name, "workflow.name");
|
|
504
556
|
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
505
557
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
@@ -515,10 +567,10 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
515
567
|
...step,
|
|
516
568
|
id: step.id,
|
|
517
569
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
518
|
-
target: validateTarget(step.target, `workflow.steps[${index}].target
|
|
570
|
+
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
519
571
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
520
572
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
521
|
-
timeoutMs:
|
|
573
|
+
timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
522
574
|
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
523
575
|
};
|
|
524
576
|
});
|
|
@@ -558,7 +610,7 @@ function workflowExecutionOrder(workflow) {
|
|
|
558
610
|
visit(step);
|
|
559
611
|
return order;
|
|
560
612
|
}
|
|
561
|
-
function workflowBodyFromJson(value, fallbackName) {
|
|
613
|
+
function workflowBodyFromJson(value, fallbackName, opts = {}) {
|
|
562
614
|
assertObject(value, "workflow file");
|
|
563
615
|
const rawName = fallbackName ?? value.name;
|
|
564
616
|
assertString(rawName, "workflow.name");
|
|
@@ -570,7 +622,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
570
622
|
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
571
623
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
572
624
|
steps: value.steps
|
|
573
|
-
});
|
|
625
|
+
}, opts);
|
|
574
626
|
}
|
|
575
627
|
|
|
576
628
|
// src/lib/run-artifacts.ts
|
|
@@ -1207,13 +1259,17 @@ class Store {
|
|
|
1207
1259
|
}
|
|
1208
1260
|
createLoop(input, from = new Date) {
|
|
1209
1261
|
const now = nowIso();
|
|
1262
|
+
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1263
|
+
name: "loop-target-validation",
|
|
1264
|
+
steps: [{ id: "target", target: input.target }]
|
|
1265
|
+
}).steps[0].target;
|
|
1210
1266
|
const loop = {
|
|
1211
1267
|
id: genId(),
|
|
1212
1268
|
name: input.name,
|
|
1213
1269
|
description: input.description,
|
|
1214
1270
|
status: "active",
|
|
1215
1271
|
schedule: input.schedule,
|
|
1216
|
-
target
|
|
1272
|
+
target,
|
|
1217
1273
|
goal: input.goal,
|
|
1218
1274
|
machine: input.machine,
|
|
1219
1275
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
@@ -1260,6 +1316,17 @@ class Store {
|
|
|
1260
1316
|
const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
|
|
1261
1317
|
return row ? rowToLoop(row) : undefined;
|
|
1262
1318
|
}
|
|
1319
|
+
requireUniqueLoop(idOrName) {
|
|
1320
|
+
const byId = this.getLoop(idOrName);
|
|
1321
|
+
if (byId)
|
|
1322
|
+
return byId;
|
|
1323
|
+
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1324
|
+
if (rows.length === 0)
|
|
1325
|
+
throw new Error(`loop not found: ${idOrName}`);
|
|
1326
|
+
if (rows.length > 1)
|
|
1327
|
+
throw new Error(`ambiguous loop name: ${idOrName}; use a loop id`);
|
|
1328
|
+
return rowToLoop(rows[0]);
|
|
1329
|
+
}
|
|
1263
1330
|
requireLoop(idOrName) {
|
|
1264
1331
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
1265
1332
|
throw new Error(`loop not found: ${idOrName}`);
|
|
@@ -1334,6 +1401,130 @@ class Store {
|
|
|
1334
1401
|
throw new Error(`loop not found after update: ${id}`);
|
|
1335
1402
|
return after;
|
|
1336
1403
|
}
|
|
1404
|
+
activeLoopReferenceCount(workflowId) {
|
|
1405
|
+
const rows = this.db.query("SELECT target_json FROM loops WHERE archived_at IS NULL").all();
|
|
1406
|
+
let count = 0;
|
|
1407
|
+
for (const row of rows) {
|
|
1408
|
+
try {
|
|
1409
|
+
const target = JSON.parse(row.target_json);
|
|
1410
|
+
if (target.type === "workflow" && target.workflowId === workflowId)
|
|
1411
|
+
count += 1;
|
|
1412
|
+
} catch {}
|
|
1413
|
+
}
|
|
1414
|
+
return count;
|
|
1415
|
+
}
|
|
1416
|
+
archiveWorkflowIfUnreferenced(workflowId, updated) {
|
|
1417
|
+
if (this.activeLoopReferenceCount(workflowId) > 0)
|
|
1418
|
+
return;
|
|
1419
|
+
const workflow = this.getWorkflow(workflowId);
|
|
1420
|
+
if (!workflow || workflow.status !== "active")
|
|
1421
|
+
return;
|
|
1422
|
+
const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(updated, workflowId);
|
|
1423
|
+
if (res.changes !== 1)
|
|
1424
|
+
return;
|
|
1425
|
+
return this.getWorkflow(workflowId);
|
|
1426
|
+
}
|
|
1427
|
+
retargetWorkflowLoop(idOrName, workflowId, opts = {}) {
|
|
1428
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1429
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1430
|
+
try {
|
|
1431
|
+
const current = this.requireLoop(idOrName);
|
|
1432
|
+
if (current.target.type !== "workflow")
|
|
1433
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1434
|
+
if (this.hasRunningRun(current.id))
|
|
1435
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1436
|
+
const workflow = this.requireWorkflow(workflowId);
|
|
1437
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1438
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1439
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1440
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1441
|
+
WHERE id=$id
|
|
1442
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1443
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1444
|
+
))`).run({
|
|
1445
|
+
$id: current.id,
|
|
1446
|
+
$target: JSON.stringify(target),
|
|
1447
|
+
$updated: updated,
|
|
1448
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1449
|
+
$now: updated
|
|
1450
|
+
});
|
|
1451
|
+
if (res.changes !== 1)
|
|
1452
|
+
throw new Error("daemon lease lost");
|
|
1453
|
+
this.db.exec("COMMIT");
|
|
1454
|
+
const after = this.getLoop(current.id);
|
|
1455
|
+
if (!after)
|
|
1456
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1457
|
+
return after;
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
try {
|
|
1460
|
+
this.db.exec("ROLLBACK");
|
|
1461
|
+
} catch {}
|
|
1462
|
+
throw error;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
|
|
1466
|
+
const normalized = normalizeCreateWorkflowInput(workflowInput);
|
|
1467
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1468
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1469
|
+
try {
|
|
1470
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1471
|
+
if (current.target.type !== "workflow")
|
|
1472
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1473
|
+
if (this.hasRunningRun(current.id))
|
|
1474
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1475
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1476
|
+
const workflow = {
|
|
1477
|
+
id: genId(),
|
|
1478
|
+
name: normalized.name,
|
|
1479
|
+
description: normalized.description,
|
|
1480
|
+
version: normalized.version ?? 1,
|
|
1481
|
+
status: "active",
|
|
1482
|
+
goal: normalized.goal,
|
|
1483
|
+
steps: normalized.steps,
|
|
1484
|
+
createdAt: updated,
|
|
1485
|
+
updatedAt: updated
|
|
1486
|
+
};
|
|
1487
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1488
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
|
|
1489
|
+
$id: workflow.id,
|
|
1490
|
+
$name: workflow.name,
|
|
1491
|
+
$description: workflow.description ?? null,
|
|
1492
|
+
$version: workflow.version,
|
|
1493
|
+
$status: workflow.status,
|
|
1494
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
1495
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1496
|
+
$created: workflow.createdAt,
|
|
1497
|
+
$updated: workflow.updatedAt
|
|
1498
|
+
});
|
|
1499
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1500
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1501
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1502
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1503
|
+
WHERE id=$id
|
|
1504
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1505
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1506
|
+
))`).run({
|
|
1507
|
+
$id: current.id,
|
|
1508
|
+
$target: JSON.stringify(target),
|
|
1509
|
+
$updated: updated,
|
|
1510
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1511
|
+
$now: updated
|
|
1512
|
+
});
|
|
1513
|
+
if (res.changes !== 1)
|
|
1514
|
+
throw new Error("daemon lease lost");
|
|
1515
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1516
|
+
this.db.exec("COMMIT");
|
|
1517
|
+
const loop = this.getLoop(current.id);
|
|
1518
|
+
if (!loop)
|
|
1519
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1520
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
try {
|
|
1523
|
+
this.db.exec("ROLLBACK");
|
|
1524
|
+
} catch {}
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1337
1528
|
renameLoop(id, name, opts = {}) {
|
|
1338
1529
|
const current = this.getLoop(id);
|
|
1339
1530
|
if (!current)
|
|
@@ -3443,7 +3634,7 @@ function commandSpec(target) {
|
|
|
3443
3634
|
cwd: commandTarget.cwd,
|
|
3444
3635
|
shell: commandTarget.shell,
|
|
3445
3636
|
env: commandTarget.env,
|
|
3446
|
-
timeoutMs: commandTarget.timeoutMs
|
|
3637
|
+
timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
|
|
3447
3638
|
idleTimeoutMs: commandTarget.idleTimeoutMs,
|
|
3448
3639
|
account: commandTarget.account,
|
|
3449
3640
|
accountTool: commandTarget.account?.tool
|
|
@@ -3454,7 +3645,7 @@ function commandSpec(target) {
|
|
|
3454
3645
|
command: providerCommand(agentTarget.provider),
|
|
3455
3646
|
args: agentArgs(agentTarget),
|
|
3456
3647
|
cwd: agentTarget.cwd,
|
|
3457
|
-
timeoutMs: agentTarget.timeoutMs ??
|
|
3648
|
+
timeoutMs: agentTarget.timeoutMs ?? null,
|
|
3458
3649
|
idleTimeoutMs: agentTarget.idleTimeoutMs,
|
|
3459
3650
|
account: agentTarget.account,
|
|
3460
3651
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
@@ -3642,12 +3833,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3642
3833
|
if (opts.signal?.aborted)
|
|
3643
3834
|
abortHandler();
|
|
3644
3835
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3645
|
-
const timer = setTimeout(() => {
|
|
3836
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3646
3837
|
timedOut = true;
|
|
3647
3838
|
if (child.pid)
|
|
3648
3839
|
killProcessGroup(child.pid);
|
|
3649
|
-
}, spec.timeoutMs);
|
|
3650
|
-
timer
|
|
3840
|
+
}, spec.timeoutMs) : undefined;
|
|
3841
|
+
timer?.unref();
|
|
3651
3842
|
let idleTimer;
|
|
3652
3843
|
const resetIdleTimer = () => {
|
|
3653
3844
|
if (!spec.idleTimeoutMs)
|
|
@@ -3679,7 +3870,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3679
3870
|
} catch (err) {
|
|
3680
3871
|
error = err instanceof Error ? err.message : String(err);
|
|
3681
3872
|
} finally {
|
|
3682
|
-
|
|
3873
|
+
if (timer)
|
|
3874
|
+
clearTimeout(timer);
|
|
3683
3875
|
if (idleTimer)
|
|
3684
3876
|
clearTimeout(idleTimer);
|
|
3685
3877
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -3821,12 +4013,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3821
4013
|
if (opts.signal?.aborted)
|
|
3822
4014
|
abortHandler();
|
|
3823
4015
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3824
|
-
const timer = setTimeout(() => {
|
|
4016
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3825
4017
|
timedOut = true;
|
|
3826
4018
|
if (child.pid)
|
|
3827
4019
|
killProcessGroup(child.pid);
|
|
3828
|
-
}, spec.timeoutMs);
|
|
3829
|
-
timer
|
|
4020
|
+
}, spec.timeoutMs) : undefined;
|
|
4021
|
+
timer?.unref();
|
|
3830
4022
|
let idleTimer;
|
|
3831
4023
|
const resetIdleTimer = () => {
|
|
3832
4024
|
if (!spec.idleTimeoutMs)
|
|
@@ -3858,7 +4050,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3858
4050
|
} catch (err) {
|
|
3859
4051
|
error = err instanceof Error ? err.message : String(err);
|
|
3860
4052
|
} finally {
|
|
3861
|
-
|
|
4053
|
+
if (timer)
|
|
4054
|
+
clearTimeout(timer);
|
|
3862
4055
|
if (idleTimer)
|
|
3863
4056
|
clearTimeout(idleTimer);
|
|
3864
4057
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -4283,7 +4476,7 @@ ${result.stderr}`);
|
|
|
4283
4476
|
// src/lib/workflow-runner.ts
|
|
4284
4477
|
function targetWithStepAccount(step) {
|
|
4285
4478
|
const account = step.account ?? step.target.account;
|
|
4286
|
-
const timeoutMs = step.timeoutMs
|
|
4479
|
+
const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
|
|
4287
4480
|
if (!account && timeoutMs === step.target.timeoutMs)
|
|
4288
4481
|
return step.target;
|
|
4289
4482
|
return { ...step.target, account, timeoutMs };
|
|
@@ -4579,7 +4772,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4579
4772
|
})
|
|
4580
4773
|
});
|
|
4581
4774
|
}
|
|
4582
|
-
const
|
|
4775
|
+
const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
|
|
4776
|
+
const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
|
|
4583
4777
|
let workflowTimedOut = false;
|
|
4584
4778
|
const externalAbort = () => controller?.abort();
|
|
4585
4779
|
if (controller && opts.signal?.aborted)
|
|
@@ -4589,13 +4783,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4589
4783
|
const timer = controller ? setTimeout(() => {
|
|
4590
4784
|
workflowTimedOut = true;
|
|
4591
4785
|
controller.abort();
|
|
4592
|
-
},
|
|
4786
|
+
}, workflowTimeoutMs) : undefined;
|
|
4593
4787
|
timer?.unref();
|
|
4594
4788
|
try {
|
|
4595
4789
|
return await executeWorkflow(store, workflow, {
|
|
4596
4790
|
...opts,
|
|
4597
4791
|
signal: controller?.signal ?? opts.signal,
|
|
4598
|
-
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${
|
|
4792
|
+
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
|
|
4599
4793
|
loop,
|
|
4600
4794
|
loopRun: run,
|
|
4601
4795
|
scheduledFor: run.scheduledFor,
|
|
@@ -4915,13 +5109,13 @@ async function tick(deps) {
|
|
|
4915
5109
|
}
|
|
4916
5110
|
|
|
4917
5111
|
// src/daemon/control.ts
|
|
4918
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5112
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4919
5113
|
import { hostname } from "os";
|
|
4920
5114
|
import { dirname as dirname2 } from "path";
|
|
4921
5115
|
|
|
4922
5116
|
// src/daemon/loop.ts
|
|
4923
5117
|
function realSleep(ms) {
|
|
4924
|
-
return new Promise((
|
|
5118
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4925
5119
|
}
|
|
4926
5120
|
async function runLoop(opts) {
|
|
4927
5121
|
const sleep = opts.sleep ?? realSleep;
|
|
@@ -4946,7 +5140,7 @@ function readPid(path = pidFilePath()) {
|
|
|
4946
5140
|
if (!existsSync3(path))
|
|
4947
5141
|
return;
|
|
4948
5142
|
try {
|
|
4949
|
-
const pid = Number(
|
|
5143
|
+
const pid = Number(readFileSync2(path, "utf8").trim());
|
|
4950
5144
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
4951
5145
|
} catch {
|
|
4952
5146
|
return;
|
|
@@ -5299,7 +5493,7 @@ function enableStartup(result) {
|
|
|
5299
5493
|
// package.json
|
|
5300
5494
|
var package_default = {
|
|
5301
5495
|
name: "@hasna/loops",
|
|
5302
|
-
version: "0.3.
|
|
5496
|
+
version: "0.3.55",
|
|
5303
5497
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5304
5498
|
type: "module",
|
|
5305
5499
|
main: "dist/index.js",
|