@hasna/loops 0.4.1 → 0.4.2

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.
@@ -1205,7 +1205,7 @@ function discardWorkflowRunManifest(staged) {
1205
1205
  var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
1206
1206
  var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
1207
1207
  var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
1208
- var SCHEMA_USER_VERSION = 6;
1208
+ var SCHEMA_USER_VERSION = 7;
1209
1209
  var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
1210
1210
  var PRUNE_BATCH_SIZE = 400;
1211
1211
  var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
@@ -1575,6 +1575,13 @@ class Store {
1575
1575
  this.addColumnIfMissing("loop_runs", "pgid", "INTEGER");
1576
1576
  this.addColumnIfMissing("loop_runs", "process_started_at", "TEXT");
1577
1577
  }
1578
+ },
1579
+ {
1580
+ id: "0007_run_claim_tokens",
1581
+ apply: () => {
1582
+ this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
1583
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
1584
+ }
1578
1585
  }
1579
1586
  ];
1580
1587
  }
@@ -1616,6 +1623,7 @@ class Store {
1616
1623
  started_at TEXT,
1617
1624
  finished_at TEXT,
1618
1625
  claimed_by TEXT,
1626
+ claim_token TEXT,
1619
1627
  lease_expires_at TEXT,
1620
1628
  pid INTEGER,
1621
1629
  pgid INTEGER,
@@ -1634,6 +1642,7 @@ class Store {
1634
1642
  CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
1635
1643
  CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
1636
1644
  CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
1645
+ CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
1637
1646
 
1638
1647
  CREATE TABLE IF NOT EXISTS daemon_lease (
1639
1648
  id TEXT PRIMARY KEY,
@@ -3505,6 +3514,7 @@ class Store {
3505
3514
  }
3506
3515
  claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
3507
3516
  const startedAt = now.toISOString();
3517
+ const claimToken = opts.claimToken ?? genId();
3508
3518
  this.db.exec("BEGIN IMMEDIATE");
3509
3519
  try {
3510
3520
  this.assertDaemonLeaseFence(opts, startedAt);
@@ -3527,12 +3537,13 @@ class Store {
3527
3537
  return;
3528
3538
  }
3529
3539
  const res3 = this.db.query(`UPDATE loop_runs SET status='running', started_at=$started, finished_at=NULL,
3530
- claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3540
+ claimed_by=$claimedBy, claim_token=$claimToken, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3531
3541
  duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
3532
3542
  WHERE id=$id AND status='running' AND lease_expires_at <= $now`).run({
3533
3543
  $id: existing.id,
3534
3544
  $started: startedAt,
3535
3545
  $claimedBy: runnerId,
3546
+ $claimToken: claimToken,
3536
3547
  $lease: leaseExpiresAt,
3537
3548
  $updated: startedAt,
3538
3549
  $now: startedAt
@@ -3541,7 +3552,7 @@ class Store {
3541
3552
  if (res3.changes !== 1)
3542
3553
  return;
3543
3554
  const run3 = this.getRun(existing.id);
3544
- return run3 ? { run: run3, loop } : undefined;
3555
+ return run3 ? { run: run3, loop, claimToken } : undefined;
3545
3556
  }
3546
3557
  if (existing.status === "succeeded" || existing.status === "skipped") {
3547
3558
  this.db.exec("COMMIT");
@@ -3549,7 +3560,7 @@ class Store {
3549
3560
  }
3550
3561
  const attempt = existing.attempt + 1;
3551
3562
  const res2 = this.db.query(`UPDATE loop_runs SET attempt=$attempt, status='running', started_at=$started, finished_at=NULL,
3552
- claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3563
+ claimed_by=$claimedBy, claim_token=$claimToken, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3553
3564
  duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
3554
3565
  WHERE id=$id
3555
3566
  AND status IN ('failed', 'timed_out', 'abandoned')
@@ -3558,6 +3569,7 @@ class Store {
3558
3569
  $attempt: attempt,
3559
3570
  $started: startedAt,
3560
3571
  $claimedBy: runnerId,
3572
+ $claimToken: claimToken,
3561
3573
  $lease: leaseExpiresAt,
3562
3574
  $updated: startedAt,
3563
3575
  $maxAttempts: loop.maxAttempts
@@ -3566,12 +3578,12 @@ class Store {
3566
3578
  if (res2.changes !== 1)
3567
3579
  return;
3568
3580
  const run2 = this.getRun(existing.id);
3569
- return run2 ? { run: run2, loop } : undefined;
3581
+ return run2 ? { run: run2, loop, claimToken } : undefined;
3570
3582
  }
3571
3583
  const id = genId();
3572
3584
  const res = this.db.query(`INSERT OR IGNORE INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
3573
- claimed_by, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
3574
- VALUES ($id, $loopId, $loopName, $scheduledFor, 1, 'running', $started, NULL, $claimedBy, $lease,
3585
+ claimed_by, claim_token, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
3586
+ VALUES ($id, $loopId, $loopName, $scheduledFor, 1, 'running', $started, NULL, $claimedBy, $claimToken, $lease,
3575
3587
  NULL, NULL, NULL, NULL, NULL, NULL, $created, $updated)`).run({
3576
3588
  $id: id,
3577
3589
  $loopId: loop.id,
@@ -3579,6 +3591,7 @@ class Store {
3579
3591
  $scheduledFor: scheduledFor,
3580
3592
  $started: startedAt,
3581
3593
  $claimedBy: runnerId,
3594
+ $claimToken: claimToken,
3582
3595
  $lease: leaseExpiresAt,
3583
3596
  $created: startedAt,
3584
3597
  $updated: startedAt
@@ -3587,7 +3600,7 @@ class Store {
3587
3600
  if (res.changes !== 1)
3588
3601
  return;
3589
3602
  const run = this.getRun(id);
3590
- return run ? { run, loop } : undefined;
3603
+ return run ? { run, loop, claimToken } : undefined;
3591
3604
  } catch (error) {
3592
3605
  try {
3593
3606
  this.db.exec("ROLLBACK");
@@ -3610,16 +3623,18 @@ class Store {
3610
3623
  $error: error ?? null,
3611
3624
  $updated: finishedAt,
3612
3625
  $claimedBy: opts.claimedBy ?? null,
3626
+ $claimToken: opts.claimToken ?? null,
3613
3627
  $now: (opts.now ?? new Date).toISOString(),
3614
3628
  $daemonLeaseId: opts.daemonLeaseId ?? null
3615
3629
  };
3616
3630
  return this.transact(() => {
3617
- const res = opts.claimedBy ? this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3631
+ const res = opts.claimedBy ? this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3618
3632
  duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3619
3633
  WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
3634
+ AND ($claimToken IS NULL OR claim_token=$claimToken)
3620
3635
  AND ($daemonLeaseId IS NULL OR EXISTS (
3621
3636
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3622
- ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3637
+ ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3623
3638
  duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3624
3639
  const run = this.getRun(id);
3625
3640
  if (!run)
@@ -3647,11 +3662,13 @@ class Store {
3647
3662
  const expiresAt = new Date(now.getTime() + leaseMs).toISOString();
3648
3663
  const res = this.db.query(`UPDATE loop_runs SET lease_expires_at=$expires, updated_at=$updated
3649
3664
  WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
3665
+ AND ($claimToken IS NULL OR claim_token=$claimToken)
3650
3666
  AND ($daemonLeaseId IS NULL OR EXISTS (
3651
3667
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3652
3668
  ))`).run({
3653
3669
  $id: id,
3654
3670
  $claimedBy: claimedBy,
3671
+ $claimToken: opts.claimToken ?? null,
3655
3672
  $expires: expiresAt,
3656
3673
  $updated: now.toISOString(),
3657
3674
  $now: now.toISOString(),
@@ -4034,11 +4051,12 @@ function publicLoop(loop) {
4034
4051
  target
4035
4052
  };
4036
4053
  }
4037
- function publicRun(run, showOutput = false) {
4054
+ function publicRun(run, showOutput = false, opts = {}) {
4038
4055
  return {
4039
4056
  ...run,
4040
4057
  stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
4041
- stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
4058
+ stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
4059
+ error: opts.redactError ? redact(run.error) : run.error
4042
4060
  };
4043
4061
  }
4044
4062
  function publicExecutorResult(result, showOutput = false) {
@@ -7605,7 +7623,7 @@ function enableStartup(result) {
7605
7623
  // package.json
7606
7624
  var package_default = {
7607
7625
  name: "@hasna/loops",
7608
- version: "0.4.1",
7626
+ version: "0.4.2",
7609
7627
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7610
7628
  type: "module",
7611
7629
  main: "dist/index.js",
@@ -7645,6 +7663,22 @@ var package_default = {
7645
7663
  "./storage": {
7646
7664
  types: "./dist/lib/store.d.ts",
7647
7665
  import: "./dist/lib/store.js"
7666
+ },
7667
+ "./storage/contract": {
7668
+ types: "./dist/lib/storage/contract.d.ts",
7669
+ import: "./dist/lib/storage/contract.js"
7670
+ },
7671
+ "./storage/sqlite": {
7672
+ types: "./dist/lib/storage/sqlite.d.ts",
7673
+ import: "./dist/lib/storage/sqlite.js"
7674
+ },
7675
+ "./storage/postgres": {
7676
+ types: "./dist/lib/storage/postgres.d.ts",
7677
+ import: "./dist/lib/storage/postgres.js"
7678
+ },
7679
+ "./storage/postgres-schema": {
7680
+ types: "./dist/lib/storage/postgres-schema.d.ts",
7681
+ import: "./dist/lib/storage/postgres-schema.js"
7648
7682
  }
7649
7683
  },
7650
7684
  files: [
@@ -7655,7 +7689,7 @@ var package_default = {
7655
7689
  "LICENSE"
7656
7690
  ],
7657
7691
  scripts: {
7658
- 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 --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",
7692
+ 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",
7659
7693
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
7660
7694
  typecheck: "tsc --noEmit",
7661
7695
  test: "bun test",
package/dist/index.d.ts CHANGED
@@ -20,6 +20,8 @@ export { computeNextAfter, initialNextRun, nextCronRun, parseCron, parseDuration
20
20
  export { runLoopNow, tick } from "./lib/scheduler.js";
21
21
  export type { ManualRunSource, RunLoopNowDeps, RunLoopNowExecuted, RunLoopNowMode, RunLoopNowResult, RunLoopNowScheduled, } from "./lib/scheduler.js";
22
22
  export { Store } from "./lib/store.js";
23
+ export { POSTGRES_MIGRATION_LEDGER_TABLE, POSTGRES_STORAGE_MIGRATIONS, PostgresStorage, SqliteLoopStorage, checksumStorageSql, createPostgresStorage, createSqliteLoopStorage, } from "./lib/storage/index.js";
24
+ export type { AppliedStorageMigration, AuditEventRecord, LoopStorageBackend, LoopStorageContract, LoopStorageMethodName, PostgresQueryExecutor, RunnerLeaseRecord, RunnerLeaseStatus, RunnerMachineRecord, RunnerMachineStatus, SchemaMigrationStorage, StorageMigration, StorageMigrationPlanItem, StorageMigrationResult, } from "./lib/storage/index.js";
23
25
  export { LOOP_DEPLOYMENT_MODES, buildDeploymentStatus, deploymentStatusLine, loopControlPlaneConfig, normalizeLoopDeploymentMode, resolveLoopDeploymentMode, } from "./lib/mode.js";
24
26
  export type { LoopControlPlaneConfig, LoopDeploymentMode, LoopDeploymentStatus, LoopModeResolution, LoopSourceOfTruth } from "./lib/mode.js";
25
27
  export { executeLoop, executeTarget, preflightTarget } from "./lib/executor.js";