@hasna/loops 0.3.38 → 0.3.39

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.
@@ -3,8 +3,9 @@
3
3
 
4
4
  // src/lib/store.ts
5
5
  import { Database } from "bun:sqlite";
6
- import { mkdirSync as mkdirSync2 } from "fs";
7
- import { dirname } from "path";
6
+ import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
7
+ import { tmpdir } from "os";
8
+ import { dirname, join as join3 } from "path";
8
9
 
9
10
  // src/lib/ids.ts
10
11
  import { randomBytes } from "crypto";
@@ -365,6 +366,8 @@ function validateTarget(value, label) {
365
366
  assertObject(value, label);
366
367
  if (value.type === "command") {
367
368
  assertString(value.command, `${label}.command`);
369
+ optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
370
+ optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
368
371
  if (value.shell !== true && /\s/.test(value.command.trim())) {
369
372
  throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
370
373
  }
@@ -376,6 +379,8 @@ function validateTarget(value, label) {
376
379
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
377
380
  if (!providers.includes(value.provider))
378
381
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
382
+ optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
383
+ optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
379
384
  if (value.authProfile !== undefined) {
380
385
  assertString(value.authProfile, `${label}.authProfile`);
381
386
  if (value.provider !== "codewith")
@@ -530,6 +535,52 @@ function workflowBodyFromJson(value, fallbackName) {
530
535
  });
531
536
  }
532
537
 
538
+ // src/lib/run-artifacts.ts
539
+ import { createHash } from "crypto";
540
+ import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
541
+ import { basename, join as join2 } from "path";
542
+ function shortHash(value) {
543
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
544
+ }
545
+ function safeRunPathSlug(value, fallback) {
546
+ const raw = value?.trim() || fallback;
547
+ const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
548
+ return slug || fallback;
549
+ }
550
+ function workflowRunSubjectKey(kind, rawSubjectRef) {
551
+ const raw = rawSubjectRef?.trim() || "subject";
552
+ const kindSlug = safeRunPathSlug(kind, "subject").slice(0, 24);
553
+ const subjectSlug = safeRunPathSlug(raw, "subject").slice(0, 48);
554
+ return `${kindSlug}-${subjectSlug}-${shortHash(`${kindSlug}
555
+ ${raw}`)}`;
556
+ }
557
+ function workflowRunProjectSlug(projectKey) {
558
+ if (!projectKey?.trim())
559
+ return "global";
560
+ return safeRunPathSlug(projectKey.startsWith("/") ? basename(projectKey) : projectKey, "project");
561
+ }
562
+ function writeWorkflowRunManifest(args) {
563
+ const projectSlug = workflowRunProjectSlug(args.projectKey);
564
+ const subjectKey = workflowRunSubjectKey(args.subjectKind, args.rawSubjectRef);
565
+ const dir = join2(args.loopsDataDir, "runs", projectSlug, subjectKey, args.workflowRunId);
566
+ mkdirSync2(dir, { recursive: true, mode: 448 });
567
+ const manifestPath = join2(dir, "manifest.json");
568
+ writeFileSync(manifestPath, JSON.stringify({
569
+ version: 1,
570
+ workflowRunId: args.workflowRunId,
571
+ workflowId: args.workflowId,
572
+ workflowName: args.workflowName,
573
+ invocationId: args.invocationId,
574
+ workItemId: args.workItemId,
575
+ projectSlug,
576
+ subjectKey,
577
+ requiredReading: [],
578
+ createdAt: new Date().toISOString(),
579
+ ...args.payload
580
+ }, null, 2), { mode: 384 });
581
+ return manifestPath;
582
+ }
583
+
533
584
  // src/lib/store.ts
534
585
  function rowToLoop(row) {
535
586
  return {
@@ -599,8 +650,11 @@ function rowToWorkflowRun(row) {
599
650
  workflowName: row.workflow_name,
600
651
  loopId: row.loop_id ?? undefined,
601
652
  loopRunId: row.loop_run_id ?? undefined,
653
+ invocationId: row.invocation_id ?? undefined,
654
+ workItemId: row.work_item_id ?? undefined,
602
655
  scheduledFor: row.scheduled_for ?? undefined,
603
656
  idempotencyKey: row.idempotency_key ?? undefined,
657
+ manifestPath: row.manifest_path ?? undefined,
604
658
  status: row.status,
605
659
  startedAt: row.started_at ?? undefined,
606
660
  finishedAt: row.finished_at ?? undefined,
@@ -611,6 +665,44 @@ function rowToWorkflowRun(row) {
611
665
  updatedAt: row.updated_at
612
666
  };
613
667
  }
668
+ function rowToWorkflowInvocation(row) {
669
+ return {
670
+ id: row.id,
671
+ workflowId: row.workflow_id ?? undefined,
672
+ templateId: row.template_id ?? undefined,
673
+ sourceRef: JSON.parse(row.source_json),
674
+ subjectRef: JSON.parse(row.subject_json),
675
+ intent: row.intent,
676
+ scope: row.scope_json ? JSON.parse(row.scope_json) : undefined,
677
+ outputPolicy: row.output_policy_json ? JSON.parse(row.output_policy_json) : undefined,
678
+ createdAt: row.created_at,
679
+ updatedAt: row.updated_at
680
+ };
681
+ }
682
+ function rowToWorkflowWorkItem(row) {
683
+ return {
684
+ id: row.id,
685
+ routeKey: row.route_key,
686
+ idempotencyKey: row.idempotency_key,
687
+ invocationId: row.invocation_id,
688
+ sourceType: row.source_type,
689
+ sourceRef: row.source_ref,
690
+ subjectRef: row.subject_ref,
691
+ projectKey: row.project_key ?? undefined,
692
+ projectGroup: row.project_group ?? undefined,
693
+ priority: row.priority,
694
+ status: row.status,
695
+ attempts: row.attempts,
696
+ nextAttemptAt: row.next_attempt_at ?? undefined,
697
+ leaseExpiresAt: row.lease_expires_at ?? undefined,
698
+ workflowId: row.workflow_id ?? undefined,
699
+ loopId: row.loop_id ?? undefined,
700
+ workflowRunId: row.workflow_run_id ?? undefined,
701
+ lastReason: row.last_reason ?? undefined,
702
+ createdAt: row.created_at,
703
+ updatedAt: row.updated_at
704
+ };
705
+ }
614
706
  function rowToWorkflowStepRun(row) {
615
707
  return {
616
708
  id: row.id,
@@ -724,13 +816,23 @@ function rowToLease(row) {
724
816
  updatedAt: row.updated_at
725
817
  };
726
818
  }
819
+ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
820
+ if (status === "succeeded")
821
+ return "succeeded";
822
+ if (["failed", "timed_out", "abandoned"].includes(status)) {
823
+ return maxAttempts !== undefined && attempt < maxAttempts ? "admitted" : "failed";
824
+ }
825
+ return;
826
+ }
727
827
 
728
828
  class Store {
729
829
  db;
830
+ rootDir;
730
831
  constructor(path) {
731
832
  const file = path ?? dbPath();
732
833
  if (file !== ":memory:")
733
- mkdirSync2(dirname(file), { recursive: true, mode: 448 });
834
+ mkdirSync3(dirname(file), { recursive: true, mode: 448 });
835
+ this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
734
836
  this.db = new Database(file);
735
837
  this.db.exec("PRAGMA busy_timeout = 5000;");
736
838
  this.db.exec("PRAGMA journal_mode = WAL;");
@@ -825,8 +927,11 @@ class Store {
825
927
  workflow_name TEXT NOT NULL,
826
928
  loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
827
929
  loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE SET NULL,
930
+ invocation_id TEXT,
931
+ work_item_id TEXT,
828
932
  scheduled_for TEXT,
829
933
  idempotency_key TEXT,
934
+ manifest_path TEXT,
830
935
  status TEXT NOT NULL,
831
936
  started_at TEXT,
832
937
  finished_at TEXT,
@@ -841,8 +946,63 @@ class Store {
841
946
  WHERE idempotency_key IS NOT NULL;
842
947
  CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_created ON workflow_runs(workflow_id, created_at DESC);
843
948
  CREATE INDEX IF NOT EXISTS idx_workflow_runs_loop_run ON workflow_runs(loop_run_id);
949
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_invocation ON workflow_runs(invocation_id);
950
+ CREATE INDEX IF NOT EXISTS idx_workflow_runs_work_item ON workflow_runs(work_item_id);
844
951
  CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
845
952
 
953
+ CREATE TABLE IF NOT EXISTS workflow_invocations (
954
+ id TEXT PRIMARY KEY,
955
+ workflow_id TEXT,
956
+ template_id TEXT,
957
+ source_kind TEXT NOT NULL,
958
+ source_id TEXT,
959
+ source_dedupe_key TEXT,
960
+ source_json TEXT NOT NULL,
961
+ subject_kind TEXT NOT NULL,
962
+ subject_id TEXT,
963
+ subject_path TEXT,
964
+ subject_url TEXT,
965
+ subject_json TEXT NOT NULL,
966
+ intent TEXT NOT NULL,
967
+ scope_json TEXT,
968
+ output_policy_json TEXT,
969
+ created_at TEXT NOT NULL,
970
+ updated_at TEXT NOT NULL
971
+ );
972
+ CREATE INDEX IF NOT EXISTS idx_workflow_invocations_source ON workflow_invocations(source_kind, source_id);
973
+ CREATE INDEX IF NOT EXISTS idx_workflow_invocations_subject ON workflow_invocations(subject_kind, subject_id, subject_path);
974
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_invocations_dedupe
975
+ ON workflow_invocations(source_kind, source_dedupe_key)
976
+ WHERE source_dedupe_key IS NOT NULL;
977
+
978
+ CREATE TABLE IF NOT EXISTS workflow_work_items (
979
+ id TEXT PRIMARY KEY,
980
+ route_key TEXT NOT NULL,
981
+ idempotency_key TEXT NOT NULL,
982
+ invocation_id TEXT NOT NULL REFERENCES workflow_invocations(id) ON DELETE CASCADE,
983
+ source_type TEXT NOT NULL,
984
+ source_ref TEXT NOT NULL,
985
+ subject_ref TEXT NOT NULL,
986
+ project_key TEXT,
987
+ project_group TEXT,
988
+ priority INTEGER NOT NULL,
989
+ status TEXT NOT NULL,
990
+ attempts INTEGER NOT NULL,
991
+ next_attempt_at TEXT,
992
+ lease_expires_at TEXT,
993
+ workflow_id TEXT REFERENCES workflow_specs(id) ON DELETE SET NULL,
994
+ loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
995
+ workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE SET NULL,
996
+ last_reason TEXT,
997
+ created_at TEXT NOT NULL,
998
+ updated_at TEXT NOT NULL,
999
+ UNIQUE(route_key, idempotency_key)
1000
+ );
1001
+ CREATE INDEX IF NOT EXISTS idx_workflow_work_items_status_next ON workflow_work_items(status, next_attempt_at, priority DESC, created_at ASC);
1002
+ CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
1003
+ CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
1004
+ CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
1005
+
846
1006
  CREATE TABLE IF NOT EXISTS workflow_step_runs (
847
1007
  id TEXT PRIMARY KEY,
848
1008
  workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
@@ -955,12 +1115,16 @@ class Store {
955
1115
  this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
956
1116
  this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
957
1117
  this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
1118
+ this.addColumnIfMissing("workflow_runs", "invocation_id", "TEXT");
1119
+ this.addColumnIfMissing("workflow_runs", "work_item_id", "TEXT");
1120
+ this.addColumnIfMissing("workflow_runs", "manifest_path", "TEXT");
958
1121
  this.addColumnIfMissing("workflow_step_runs", "pid", "INTEGER");
959
1122
  this.addColumnIfMissing("workflow_step_runs", "goal_run_id", "TEXT");
960
1123
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
961
1124
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
962
1125
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
963
1126
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
1127
+ this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0005_workflow_invocations_and_admission", nowIso());
964
1128
  }
965
1129
  addColumnIfMissing(table, column, definition) {
966
1130
  const columns = this.db.query(`PRAGMA table_info(${table})`).all();
@@ -1083,6 +1247,10 @@ class Store {
1083
1247
  $daemonLeaseId: opts.daemonLeaseId ?? null,
1084
1248
  $now: updated
1085
1249
  });
1250
+ if (patch.status && patch.status !== "active") {
1251
+ const status = patch.status === "paused" ? "deferred" : "cancelled";
1252
+ this.setWorkflowWorkItemsForLoop(id, status, `loop ${patch.status}`, updated);
1253
+ }
1086
1254
  const after = this.getLoop(id);
1087
1255
  if (!after)
1088
1256
  throw new Error(`loop not found after update: ${id}`);
@@ -1127,6 +1295,7 @@ class Store {
1127
1295
  $archivedFromStatus: loop.status,
1128
1296
  $updated: updated
1129
1297
  });
1298
+ this.setWorkflowWorkItemsForLoop(loop.id, "deferred", "loop archived", updated);
1130
1299
  const archived = this.getLoop(loop.id);
1131
1300
  if (!archived)
1132
1301
  throw new Error(`loop not found after archive: ${loop.id}`);
@@ -1152,6 +1321,7 @@ class Store {
1152
1321
  }
1153
1322
  deleteLoop(idOrName) {
1154
1323
  const loop = this.requireLoop(idOrName);
1324
+ this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
1155
1325
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
1156
1326
  return res.changes > 0;
1157
1327
  }
@@ -1210,6 +1380,185 @@ class Store {
1210
1380
  throw new Error(`workflow not found after archive: ${workflow.id}`);
1211
1381
  return archived;
1212
1382
  }
1383
+ createWorkflowInvocation(input) {
1384
+ const now = nowIso();
1385
+ const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
1386
+ if (sourceDedupeKey) {
1387
+ const existing = this.db.query("SELECT * FROM workflow_invocations WHERE source_kind = ? AND source_dedupe_key = ? LIMIT 1").get(input.sourceRef.kind, sourceDedupeKey);
1388
+ if (existing)
1389
+ return rowToWorkflowInvocation(existing);
1390
+ }
1391
+ const id = input.id ?? genId();
1392
+ this.db.query(`INSERT INTO workflow_invocations (id, workflow_id, template_id, source_kind, source_id, source_dedupe_key,
1393
+ source_json, subject_kind, subject_id, subject_path, subject_url, subject_json, intent, scope_json,
1394
+ output_policy_json, created_at, updated_at)
1395
+ VALUES ($id, $workflowId, $templateId, $sourceKind, $sourceId, $sourceDedupeKey, $sourceJson,
1396
+ $subjectKind, $subjectId, $subjectPath, $subjectUrl, $subjectJson, $intent, $scopeJson,
1397
+ $outputPolicyJson, $created, $updated)`).run({
1398
+ $id: id,
1399
+ $workflowId: input.workflowId ?? null,
1400
+ $templateId: input.templateId ?? null,
1401
+ $sourceKind: input.sourceRef.kind,
1402
+ $sourceId: input.sourceRef.id ?? null,
1403
+ $sourceDedupeKey: sourceDedupeKey ?? null,
1404
+ $sourceJson: JSON.stringify(input.sourceRef),
1405
+ $subjectKind: input.subjectRef.kind,
1406
+ $subjectId: input.subjectRef.id ?? null,
1407
+ $subjectPath: input.subjectRef.path ?? null,
1408
+ $subjectUrl: input.subjectRef.url ?? null,
1409
+ $subjectJson: JSON.stringify(input.subjectRef),
1410
+ $intent: input.intent,
1411
+ $scopeJson: input.scope ? JSON.stringify(input.scope) : null,
1412
+ $outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
1413
+ $created: now,
1414
+ $updated: now
1415
+ });
1416
+ const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
1417
+ if (!row)
1418
+ throw new Error(`workflow invocation not found after create: ${id}`);
1419
+ return rowToWorkflowInvocation(row);
1420
+ }
1421
+ getWorkflowInvocation(id) {
1422
+ const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
1423
+ return row ? rowToWorkflowInvocation(row) : undefined;
1424
+ }
1425
+ listWorkflowInvocations(opts = {}) {
1426
+ const rows = this.db.query("SELECT * FROM workflow_invocations ORDER BY created_at DESC LIMIT ?").all(opts.limit ?? 100);
1427
+ return rows.map(rowToWorkflowInvocation);
1428
+ }
1429
+ upsertWorkflowWorkItem(input) {
1430
+ const now = nowIso();
1431
+ const id = genId();
1432
+ const status = input.status ?? "queued";
1433
+ this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
1434
+ subject_ref, project_key, project_group, priority, status, attempts, next_attempt_at, lease_expires_at,
1435
+ workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
1436
+ VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
1437
+ $projectKey, $projectGroup, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
1438
+ $lastReason, $created, $updated)
1439
+ ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
1440
+ invocation_id=excluded.invocation_id,
1441
+ source_type=excluded.source_type,
1442
+ source_ref=excluded.source_ref,
1443
+ subject_ref=excluded.subject_ref,
1444
+ project_key=excluded.project_key,
1445
+ project_group=excluded.project_group,
1446
+ priority=excluded.priority,
1447
+ status=CASE
1448
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
1449
+ THEN workflow_work_items.status
1450
+ ELSE excluded.status
1451
+ END,
1452
+ workflow_id=CASE
1453
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
1454
+ ELSE NULL
1455
+ END,
1456
+ loop_id=CASE
1457
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
1458
+ ELSE NULL
1459
+ END,
1460
+ workflow_run_id=CASE
1461
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
1462
+ ELSE NULL
1463
+ END,
1464
+ lease_expires_at=CASE
1465
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
1466
+ ELSE NULL
1467
+ END,
1468
+ next_attempt_at=excluded.next_attempt_at,
1469
+ last_reason=COALESCE(excluded.last_reason, workflow_work_items.last_reason),
1470
+ updated_at=excluded.updated_at`).run({
1471
+ $id: id,
1472
+ $routeKey: input.routeKey,
1473
+ $idempotencyKey: input.idempotencyKey,
1474
+ $invocationId: input.invocationId,
1475
+ $sourceType: input.sourceType,
1476
+ $sourceRef: input.sourceRef,
1477
+ $subjectRef: input.subjectRef,
1478
+ $projectKey: input.projectKey ?? null,
1479
+ $projectGroup: input.projectGroup ?? null,
1480
+ $priority: input.priority ?? 0,
1481
+ $status: status,
1482
+ $nextAttemptAt: input.nextAttemptAt ?? null,
1483
+ $lastReason: input.lastReason ?? null,
1484
+ $created: now,
1485
+ $updated: now
1486
+ });
1487
+ const row = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? AND idempotency_key = ? LIMIT 1").get(input.routeKey, input.idempotencyKey);
1488
+ if (!row)
1489
+ throw new Error(`workflow work item not found after upsert: ${input.routeKey}/${input.idempotencyKey}`);
1490
+ return rowToWorkflowWorkItem(row);
1491
+ }
1492
+ getWorkflowWorkItem(id) {
1493
+ const row = this.db.query("SELECT * FROM workflow_work_items WHERE id = ?").get(id);
1494
+ return row ? rowToWorkflowWorkItem(row) : undefined;
1495
+ }
1496
+ findWorkflowWorkItem(routeKey, idempotencyKey) {
1497
+ const row = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? AND idempotency_key = ? LIMIT 1").get(routeKey, idempotencyKey);
1498
+ return row ? rowToWorkflowWorkItem(row) : undefined;
1499
+ }
1500
+ listWorkflowWorkItems(opts = {}) {
1501
+ const limit = opts.limit ?? 100;
1502
+ let rows;
1503
+ if (opts.status && opts.routeKey) {
1504
+ rows = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? AND status = ? ORDER BY priority DESC, created_at ASC LIMIT ?").all(opts.routeKey, opts.status, limit);
1505
+ } else if (opts.status) {
1506
+ rows = this.db.query("SELECT * FROM workflow_work_items WHERE status = ? ORDER BY priority DESC, created_at ASC LIMIT ?").all(opts.status, limit);
1507
+ } else if (opts.routeKey) {
1508
+ rows = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? ORDER BY created_at DESC LIMIT ?").all(opts.routeKey, limit);
1509
+ } else {
1510
+ rows = this.db.query("SELECT * FROM workflow_work_items ORDER BY created_at DESC LIMIT ?").all(limit);
1511
+ }
1512
+ return rows.map(rowToWorkflowWorkItem);
1513
+ }
1514
+ countActiveWorkflowWorkItems(args = {}) {
1515
+ const active = ["admitted", "running"];
1516
+ const placeholders = active.map(() => "?").join(",");
1517
+ const global = this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders})`).get(...active)?.count ?? 0;
1518
+ const project = args.projectKey ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_key = ?`).get(...active, args.projectKey)?.count ?? 0 : 0;
1519
+ const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
1520
+ return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
1521
+ }
1522
+ admitWorkflowWorkItem(id, patch) {
1523
+ const now = nowIso();
1524
+ const res = this.db.query(`UPDATE workflow_work_items
1525
+ SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
1526
+ next_attempt_at=NULL, lease_expires_at=NULL, last_reason=$reason, updated_at=$updated
1527
+ WHERE id=$id AND status IN ('queued', 'deferred')`).run({
1528
+ $id: id,
1529
+ $workflowId: patch.workflowId,
1530
+ $loopId: patch.loopId,
1531
+ $reason: patch.reason ?? null,
1532
+ $updated: now
1533
+ });
1534
+ const item = this.getWorkflowWorkItem(id);
1535
+ if (!item)
1536
+ throw new Error(`workflow work item not found after admit: ${id}`);
1537
+ if (res.changes !== 1)
1538
+ throw new Error(`workflow work item is not claimable: ${id} status=${item.status}`);
1539
+ return item;
1540
+ }
1541
+ setWorkflowWorkItemsForLoop(loopId, status, reason, updated, statuses = ["admitted", "running"]) {
1542
+ const placeholders = statuses.map(() => "?").join(",");
1543
+ this.db.query(`UPDATE workflow_work_items
1544
+ SET status=?, lease_expires_at=NULL, last_reason=COALESCE(?, last_reason), updated_at=?
1545
+ WHERE loop_id = ? AND status IN (${placeholders})`).run(status, reason ?? null, updated, loopId, ...statuses);
1546
+ }
1547
+ setWorkflowWorkItemsForWorkflowRun(workflowRunId, status, reason, updated, statuses = ["admitted", "running"]) {
1548
+ const placeholders = statuses.map(() => "?").join(",");
1549
+ this.db.query(`UPDATE workflow_work_items
1550
+ SET status=?, lease_expires_at=NULL, last_reason=COALESCE(?, last_reason), updated_at=?
1551
+ WHERE workflow_run_id = ? AND status IN (${placeholders})`).run(status, reason ?? null, updated, workflowRunId, ...statuses);
1552
+ }
1553
+ setWorkflowWorkItemsForLoopRun(run, reason, updated) {
1554
+ const loop = this.getLoop(run.loopId);
1555
+ const status = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
1556
+ if (!status)
1557
+ return;
1558
+ const statuses = status === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
1559
+ const nextReason = status === "admitted" ? reason ? `attempt failed; retry pending: ${reason}` : "attempt failed; retry pending" : reason;
1560
+ this.setWorkflowWorkItemsForLoop(run.loopId, status, nextReason, updated, statuses);
1561
+ }
1213
1562
  createGoal(input, opts = {}) {
1214
1563
  const now = nowIso();
1215
1564
  this.db.exec("BEGIN IMMEDIATE");
@@ -1483,6 +1832,10 @@ class Store {
1483
1832
  }
1484
1833
  createWorkflowRun(input) {
1485
1834
  const now = nowIso();
1835
+ const targetInput = input.loop?.target.type === "workflow" ? input.loop.target.input : undefined;
1836
+ const invocationId = input.invocationId ?? targetInput?.workflowInvocationId ?? targetInput?.invocationId;
1837
+ const workItemId = input.workItemId ?? targetInput?.workflowWorkItemId ?? targetInput?.workItemId;
1838
+ let manifestPath;
1486
1839
  if (input.idempotencyKey) {
1487
1840
  const existing = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? AND idempotency_key = ? LIMIT 1").get(input.workflow.id, input.idempotencyKey);
1488
1841
  if (existing) {
@@ -1501,21 +1854,59 @@ class Store {
1501
1854
  }
1502
1855
  }
1503
1856
  const runId = genId();
1504
- this.db.query(`INSERT INTO workflow_runs (id, workflow_id, workflow_name, loop_id, loop_run_id, scheduled_for, idempotency_key,
1505
- status, started_at, finished_at, duration_ms, error, created_at, updated_at)
1506
- VALUES ($id, $workflowId, $workflowName, $loopId, $loopRunId, $scheduledFor, $idempotencyKey,
1507
- 'running', $started, NULL, NULL, NULL, $created, $updated)`).run({
1857
+ const workItem = workItemId ? this.getWorkflowWorkItem(workItemId) : undefined;
1858
+ const invocation = invocationId ? this.getWorkflowInvocation(invocationId) : undefined;
1859
+ manifestPath = invocation || workItem ? writeWorkflowRunManifest({
1860
+ loopsDataDir: this.rootDir,
1861
+ workflowRunId: runId,
1862
+ workflowId: input.workflow.id,
1863
+ workflowName: input.workflow.name,
1864
+ invocationId,
1865
+ workItemId,
1866
+ projectKey: workItem?.projectKey ?? invocation?.scope?.projectPath,
1867
+ subjectKind: invocation?.subjectRef.kind,
1868
+ rawSubjectRef: workItem?.subjectRef ?? invocation?.subjectRef.path ?? invocation?.subjectRef.id ?? invocation?.subjectRef.url,
1869
+ payload: {
1870
+ workflowInvocation: invocation,
1871
+ workflowWorkItem: workItem,
1872
+ loopId: input.loop?.id,
1873
+ loopRunId: input.loopRun?.id,
1874
+ scheduledFor: input.scheduledFor ?? input.loopRun?.scheduledFor
1875
+ }
1876
+ }) : undefined;
1877
+ this.db.query(`INSERT INTO workflow_runs (id, workflow_id, workflow_name, loop_id, loop_run_id, invocation_id, work_item_id,
1878
+ scheduled_for, idempotency_key, manifest_path, status, started_at, finished_at, duration_ms, error,
1879
+ created_at, updated_at)
1880
+ VALUES ($id, $workflowId, $workflowName, $loopId, $loopRunId, $invocationId, $workItemId, $scheduledFor,
1881
+ $idempotencyKey, $manifestPath, 'running', $started, NULL, NULL, NULL, $created, $updated)`).run({
1508
1882
  $id: runId,
1509
1883
  $workflowId: input.workflow.id,
1510
1884
  $workflowName: input.workflow.name,
1511
1885
  $loopId: input.loop?.id ?? null,
1512
1886
  $loopRunId: input.loopRun?.id ?? null,
1887
+ $invocationId: invocationId ?? null,
1888
+ $workItemId: workItemId ?? null,
1513
1889
  $scheduledFor: input.scheduledFor ?? input.loopRun?.scheduledFor ?? null,
1514
1890
  $idempotencyKey: input.idempotencyKey ?? null,
1891
+ $manifestPath: manifestPath ?? null,
1515
1892
  $started: now,
1516
1893
  $created: now,
1517
1894
  $updated: now
1518
1895
  });
1896
+ if (workItemId) {
1897
+ const workItemRes = this.db.query(`UPDATE workflow_work_items
1898
+ SET status='running', workflow_run_id=$workflowRunId, lease_expires_at=$leaseExpiresAt, updated_at=$updated
1899
+ WHERE id=$id AND status IN ('admitted', 'queued', 'deferred', 'running')`).run({
1900
+ $id: workItemId,
1901
+ $workflowRunId: runId,
1902
+ $leaseExpiresAt: input.loop ? new Date(Date.now() + input.loop.leaseMs).toISOString() : null,
1903
+ $updated: now
1904
+ });
1905
+ if (workItemRes.changes !== 1) {
1906
+ const current = this.getWorkflowWorkItem(workItemId);
1907
+ throw new Error(`workflow work item is not runnable: ${workItemId}${current ? ` status=${current.status}` : ""}`);
1908
+ }
1909
+ }
1519
1910
  input.workflow.steps.forEach((step, sequence) => {
1520
1911
  const account = step.account ?? step.target.account;
1521
1912
  this.db.query(`INSERT INTO workflow_step_runs (id, workflow_run_id, step_id, sequence, status, started_at, finished_at,
@@ -1541,7 +1932,10 @@ class Store {
1541
1932
  workflowName: input.workflow.name,
1542
1933
  stepCount: input.workflow.steps.length,
1543
1934
  loopId: input.loop?.id,
1544
- loopRunId: input.loopRun?.id
1935
+ loopRunId: input.loopRun?.id,
1936
+ invocationId,
1937
+ workItemId,
1938
+ manifestPath
1545
1939
  }),
1546
1940
  $created: now
1547
1941
  });
@@ -1554,6 +1948,8 @@ class Store {
1554
1948
  try {
1555
1949
  this.db.exec("ROLLBACK");
1556
1950
  } catch {}
1951
+ if (manifestPath)
1952
+ rmSync(manifestPath, { force: true });
1557
1953
  throw error;
1558
1954
  }
1559
1955
  }
@@ -1766,6 +2162,10 @@ class Store {
1766
2162
  changed = res.changes === 1;
1767
2163
  if (changed)
1768
2164
  this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
2165
+ if (changed) {
2166
+ const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
2167
+ this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
2168
+ }
1769
2169
  this.db.exec("COMMIT");
1770
2170
  } catch (error) {
1771
2171
  try {
@@ -1790,6 +2190,7 @@ class Store {
1790
2190
  this.db.query(`UPDATE workflow_step_runs
1791
2191
  SET status='cancelled', finished_at=$finished, pid=NULL, error=$reason, updated_at=$updated
1792
2192
  WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
2193
+ this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
1793
2194
  this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
1794
2195
  }
1795
2196
  this.db.exec("COMMIT");
@@ -2043,6 +2444,8 @@ class Store {
2043
2444
  throw new Error(`run not found after finalize: ${id}`);
2044
2445
  if (opts.claimedBy && res.changes !== 1)
2045
2446
  return run;
2447
+ if (res.changes === 1)
2448
+ this.setWorkflowWorkItemsForLoopRun(run, patch.error, finishedAt);
2046
2449
  return run;
2047
2450
  }
2048
2451
  heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
@@ -2136,6 +2539,14 @@ class Store {
2136
2539
  error: "parent loop run lease expired before completion",
2137
2540
  loopRunId: row.id
2138
2541
  });
2542
+ this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
2543
+ }
2544
+ const loop = this.getLoop(row.loop_id);
2545
+ const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
2546
+ if (itemStatus) {
2547
+ const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
2548
+ const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
2549
+ this.setWorkflowWorkItemsForLoop(row.loop_id, itemStatus, reason, finished, statuses);
2139
2550
  }
2140
2551
  this.db.exec("COMMIT");
2141
2552
  } catch (error) {
@@ -2356,7 +2767,7 @@ function resolveAccountEnv(account, toolHint, env) {
2356
2767
  // src/lib/env.ts
2357
2768
  import { accessSync, constants } from "fs";
2358
2769
  import { homedir as homedir2 } from "os";
2359
- import { delimiter, join as join2 } from "path";
2770
+ import { delimiter, join as join4 } from "path";
2360
2771
  function compactPathParts(parts) {
2361
2772
  const seen = new Set;
2362
2773
  const result = [];
@@ -2372,14 +2783,14 @@ function compactPathParts(parts) {
2372
2783
  function commonExecutableDirs(env = process.env) {
2373
2784
  const home = env.HOME || homedir2();
2374
2785
  return compactPathParts([
2375
- join2(home, ".local", "bin"),
2376
- join2(home, ".bun", "bin"),
2377
- join2(home, ".cargo", "bin"),
2378
- join2(home, ".npm-global", "bin"),
2379
- join2(home, "bin"),
2380
- env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
2786
+ join4(home, ".local", "bin"),
2787
+ join4(home, ".bun", "bin"),
2788
+ join4(home, ".cargo", "bin"),
2789
+ join4(home, ".npm-global", "bin"),
2790
+ join4(home, "bin"),
2791
+ env.BUN_INSTALL ? join4(env.BUN_INSTALL, "bin") : undefined,
2381
2792
  env.PNPM_HOME,
2382
- env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
2793
+ env.NPM_CONFIG_PREFIX ? join4(env.NPM_CONFIG_PREFIX, "bin") : undefined,
2383
2794
  "/opt/homebrew/bin",
2384
2795
  "/usr/local/bin",
2385
2796
  "/usr/bin",
@@ -2403,7 +2814,7 @@ function executableExists(command, env = process.env) {
2403
2814
  if (command.includes("/"))
2404
2815
  return isExecutable(command);
2405
2816
  for (const dir of (env.PATH ?? "").split(delimiter)) {
2406
- if (dir && isExecutable(join2(dir, command)))
2817
+ if (dir && isExecutable(join4(dir, command)))
2407
2818
  return true;
2408
2819
  }
2409
2820
  return false;
@@ -2761,6 +3172,7 @@ function commandSpec(target) {
2761
3172
  shell: commandTarget.shell,
2762
3173
  env: commandTarget.env,
2763
3174
  timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3175
+ idleTimeoutMs: commandTarget.idleTimeoutMs,
2764
3176
  account: commandTarget.account,
2765
3177
  accountTool: commandTarget.account?.tool
2766
3178
  };
@@ -2771,6 +3183,7 @@ function commandSpec(target) {
2771
3183
  args: agentArgs(agentTarget),
2772
3184
  cwd: agentTarget.cwd,
2773
3185
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3186
+ idleTimeoutMs: agentTarget.idleTimeoutMs,
2774
3187
  account: agentTarget.account,
2775
3188
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2776
3189
  nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
@@ -2918,6 +3331,7 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
2918
3331
  let stdout = "";
2919
3332
  let stderr = "";
2920
3333
  let timedOut = false;
3334
+ let idleTimedOut = false;
2921
3335
  let exitCode;
2922
3336
  let error;
2923
3337
  let plan;
@@ -2956,18 +3370,34 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
2956
3370
  if (opts.signal?.aborted)
2957
3371
  abortHandler();
2958
3372
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
2959
- child.stdout?.on("data", (chunk) => {
2960
- stdout = appendBounded(stdout, chunk, maxOutputBytes);
2961
- });
2962
- child.stderr?.on("data", (chunk) => {
2963
- stderr = appendBounded(stderr, chunk, maxOutputBytes);
2964
- });
2965
3373
  const timer = setTimeout(() => {
2966
3374
  timedOut = true;
2967
3375
  if (child.pid)
2968
3376
  killProcessGroup(child.pid);
2969
3377
  }, spec.timeoutMs);
2970
3378
  timer.unref();
3379
+ let idleTimer;
3380
+ const resetIdleTimer = () => {
3381
+ if (!spec.idleTimeoutMs)
3382
+ return;
3383
+ if (idleTimer)
3384
+ clearTimeout(idleTimer);
3385
+ idleTimer = setTimeout(() => {
3386
+ idleTimedOut = true;
3387
+ if (child.pid)
3388
+ killProcessGroup(child.pid);
3389
+ }, spec.idleTimeoutMs);
3390
+ idleTimer.unref();
3391
+ };
3392
+ resetIdleTimer();
3393
+ child.stdout?.on("data", (chunk) => {
3394
+ stdout = appendBounded(stdout, chunk, maxOutputBytes);
3395
+ resetIdleTimer();
3396
+ });
3397
+ child.stderr?.on("data", (chunk) => {
3398
+ stderr = appendBounded(stderr, chunk, maxOutputBytes);
3399
+ resetIdleTimer();
3400
+ });
2971
3401
  try {
2972
3402
  const [code, signal] = await once(child, "exit");
2973
3403
  if (typeof code === "number")
@@ -2978,17 +3408,19 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
2978
3408
  error = err instanceof Error ? err.message : String(err);
2979
3409
  } finally {
2980
3410
  clearTimeout(timer);
3411
+ if (idleTimer)
3412
+ clearTimeout(idleTimer);
2981
3413
  opts.signal?.removeEventListener("abort", abortHandler);
2982
3414
  }
2983
3415
  const finishedAt = nowIso();
2984
3416
  const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
2985
- if (timedOut) {
3417
+ if (timedOut || idleTimedOut) {
2986
3418
  return {
2987
3419
  status: "timed_out",
2988
3420
  exitCode,
2989
3421
  stdout,
2990
3422
  stderr,
2991
- error: `timed out after ${spec.timeoutMs}ms`,
3423
+ error: idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`,
2992
3424
  pid: child.pid,
2993
3425
  startedAt,
2994
3426
  finishedAt,
@@ -3054,6 +3486,7 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3054
3486
  let stdout = "";
3055
3487
  let stderr = "";
3056
3488
  let timedOut = false;
3489
+ let idleTimedOut = false;
3057
3490
  let exitCode;
3058
3491
  let error;
3059
3492
  const env = executionEnv(spec, metadata, opts);
@@ -3116,18 +3549,34 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3116
3549
  if (opts.signal?.aborted)
3117
3550
  abortHandler();
3118
3551
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
3119
- child.stdout?.on("data", (chunk) => {
3120
- stdout = appendBounded(stdout, chunk, maxOutputBytes);
3121
- });
3122
- child.stderr?.on("data", (chunk) => {
3123
- stderr = appendBounded(stderr, chunk, maxOutputBytes);
3124
- });
3125
3552
  const timer = setTimeout(() => {
3126
3553
  timedOut = true;
3127
3554
  if (child.pid)
3128
3555
  killProcessGroup(child.pid);
3129
3556
  }, spec.timeoutMs);
3130
3557
  timer.unref();
3558
+ let idleTimer;
3559
+ const resetIdleTimer = () => {
3560
+ if (!spec.idleTimeoutMs)
3561
+ return;
3562
+ if (idleTimer)
3563
+ clearTimeout(idleTimer);
3564
+ idleTimer = setTimeout(() => {
3565
+ idleTimedOut = true;
3566
+ if (child.pid)
3567
+ killProcessGroup(child.pid);
3568
+ }, spec.idleTimeoutMs);
3569
+ idleTimer.unref();
3570
+ };
3571
+ resetIdleTimer();
3572
+ child.stdout?.on("data", (chunk) => {
3573
+ stdout = appendBounded(stdout, chunk, maxOutputBytes);
3574
+ resetIdleTimer();
3575
+ });
3576
+ child.stderr?.on("data", (chunk) => {
3577
+ stderr = appendBounded(stderr, chunk, maxOutputBytes);
3578
+ resetIdleTimer();
3579
+ });
3131
3580
  try {
3132
3581
  const [code, signal] = await once(child, "exit");
3133
3582
  if (typeof code === "number")
@@ -3138,17 +3587,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3138
3587
  error = err instanceof Error ? err.message : String(err);
3139
3588
  } finally {
3140
3589
  clearTimeout(timer);
3590
+ if (idleTimer)
3591
+ clearTimeout(idleTimer);
3141
3592
  opts.signal?.removeEventListener("abort", abortHandler);
3142
3593
  }
3143
3594
  const finishedAt = nowIso();
3144
3595
  const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
3145
- if (timedOut) {
3596
+ if (timedOut || idleTimedOut) {
3146
3597
  return {
3147
3598
  status: "timed_out",
3148
3599
  exitCode,
3149
3600
  stdout,
3150
3601
  stderr,
3151
- error: `timed out after ${spec.timeoutMs}ms`,
3602
+ error: idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`,
3152
3603
  pid: child.pid,
3153
3604
  startedAt,
3154
3605
  finishedAt,
@@ -4190,7 +4641,7 @@ async function tick(deps) {
4190
4641
  }
4191
4642
 
4192
4643
  // src/daemon/control.ts
4193
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
4644
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4194
4645
  import { hostname } from "os";
4195
4646
  import { dirname as dirname2 } from "path";
4196
4647
 
@@ -4228,11 +4679,11 @@ function readPid(path = pidFilePath()) {
4228
4679
  }
4229
4680
  }
4230
4681
  function writePid(pid = process.pid, path = pidFilePath()) {
4231
- mkdirSync3(dirname2(path), { recursive: true, mode: 448 });
4232
- writeFileSync(path, String(pid));
4682
+ mkdirSync4(dirname2(path), { recursive: true, mode: 448 });
4683
+ writeFileSync2(path, String(pid));
4233
4684
  }
4234
4685
  function removePid(path = pidFilePath()) {
4235
- rmSync(path, { force: true });
4686
+ rmSync2(path, { force: true });
4236
4687
  }
4237
4688
  function isAlive(pid) {
4238
4689
  try {
@@ -4488,7 +4939,7 @@ async function startDaemon(opts) {
4488
4939
  }
4489
4940
 
4490
4941
  // src/daemon/install.ts
4491
- import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
4942
+ import { chmodSync, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
4492
4943
  import { spawnSync as spawnSync3 } from "child_process";
4493
4944
  import { dirname as dirname3 } from "path";
4494
4945
  function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
@@ -4496,8 +4947,8 @@ function installStartup(cliEntry, execPath = process.execPath, args = ["daemon",
4496
4947
  const pathEnv = normalizeExecutionPath(process.env);
4497
4948
  if (process.platform === "linux") {
4498
4949
  const path = systemdServicePath();
4499
- mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
4500
- writeFileSync2(path, `[Unit]
4950
+ mkdirSync5(dirname3(path), { recursive: true, mode: 448 });
4951
+ writeFileSync3(path, `[Unit]
4501
4952
  Description=Hasna OpenLoops daemon
4502
4953
  After=default.target
4503
4954
 
@@ -4523,8 +4974,8 @@ WantedBy=default.target
4523
4974
  }
4524
4975
  if (process.platform === "darwin") {
4525
4976
  const path = launchdPlistPath();
4526
- mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
4527
- writeFileSync2(path, `<?xml version="1.0" encoding="UTF-8"?>
4977
+ mkdirSync5(dirname3(path), { recursive: true, mode: 448 });
4978
+ writeFileSync3(path, `<?xml version="1.0" encoding="UTF-8"?>
4528
4979
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4529
4980
  <plist version="1.0">
4530
4981
  <dict>
@@ -4574,7 +5025,7 @@ function enableStartup(result) {
4574
5025
  // package.json
4575
5026
  var package_default = {
4576
5027
  name: "@hasna/loops",
4577
- version: "0.3.38",
5028
+ version: "0.3.39",
4578
5029
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4579
5030
  type: "module",
4580
5031
  main: "dist/index.js",