@hasna/loops 0.4.1 → 0.4.3

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.
@@ -2,6 +2,7 @@ import type { CreateLoopInput, CreateWorkflowInvocationInput, CreateWorkflowInpu
2
2
  interface DaemonLeaseFence {
3
3
  daemonLeaseId?: string;
4
4
  now?: Date;
5
+ claimToken?: string;
5
6
  }
6
7
  export interface DaemonLease {
7
8
  id: string;
@@ -15,6 +16,7 @@ export interface DaemonLease {
15
16
  export interface ClaimRunResult {
16
17
  run: LoopRun;
17
18
  loop: Loop;
19
+ claimToken?: string;
18
20
  }
19
21
  export interface CreateWorkflowRunInput {
20
22
  workflow: WorkflowSpec;
@@ -270,6 +272,7 @@ export declare class Store {
270
272
  claimedBy?: string;
271
273
  now?: Date;
272
274
  daemonLeaseId?: string;
275
+ claimToken?: string;
273
276
  }): LoopRun;
274
277
  heartbeatRunLease(id: string, claimedBy: string, leaseMs: number, now?: Date, opts?: DaemonLeaseFence): LoopRun | undefined;
275
278
  listRuns(opts?: {
package/dist/lib/store.js CHANGED
@@ -1203,7 +1203,7 @@ function discardWorkflowRunManifest(staged) {
1203
1203
  var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
1204
1204
  var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
1205
1205
  var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
1206
- var SCHEMA_USER_VERSION = 6;
1206
+ var SCHEMA_USER_VERSION = 7;
1207
1207
  var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
1208
1208
  var PRUNE_BATCH_SIZE = 400;
1209
1209
  var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
@@ -1523,7 +1523,7 @@ class Store {
1523
1523
  }
1524
1524
  const applied = new Set(this.db.query("SELECT id FROM schema_migrations").all().map((row) => row.id));
1525
1525
  for (const migration of this.migrations()) {
1526
- if (!migration.baseline && applied.has(migration.id))
1526
+ if (!migration.baseline && !migration.reapply && applied.has(migration.id))
1527
1527
  continue;
1528
1528
  migration.apply();
1529
1529
  if (!applied.has(migration.id)) {
@@ -1573,6 +1573,14 @@ class Store {
1573
1573
  this.addColumnIfMissing("loop_runs", "pgid", "INTEGER");
1574
1574
  this.addColumnIfMissing("loop_runs", "process_started_at", "TEXT");
1575
1575
  }
1576
+ },
1577
+ {
1578
+ id: "0007_run_claim_tokens",
1579
+ reapply: true,
1580
+ apply: () => {
1581
+ this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
1582
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
1583
+ }
1576
1584
  }
1577
1585
  ];
1578
1586
  }
@@ -1614,6 +1622,7 @@ class Store {
1614
1622
  started_at TEXT,
1615
1623
  finished_at TEXT,
1616
1624
  claimed_by TEXT,
1625
+ claim_token TEXT,
1617
1626
  lease_expires_at TEXT,
1618
1627
  pid INTEGER,
1619
1628
  pgid INTEGER,
@@ -3503,6 +3512,7 @@ class Store {
3503
3512
  }
3504
3513
  claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
3505
3514
  const startedAt = now.toISOString();
3515
+ const claimToken = opts.claimToken ?? genId();
3506
3516
  this.db.exec("BEGIN IMMEDIATE");
3507
3517
  try {
3508
3518
  this.assertDaemonLeaseFence(opts, startedAt);
@@ -3525,12 +3535,13 @@ class Store {
3525
3535
  return;
3526
3536
  }
3527
3537
  const res3 = this.db.query(`UPDATE loop_runs SET status='running', started_at=$started, finished_at=NULL,
3528
- claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3538
+ claimed_by=$claimedBy, claim_token=$claimToken, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3529
3539
  duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
3530
3540
  WHERE id=$id AND status='running' AND lease_expires_at <= $now`).run({
3531
3541
  $id: existing.id,
3532
3542
  $started: startedAt,
3533
3543
  $claimedBy: runnerId,
3544
+ $claimToken: claimToken,
3534
3545
  $lease: leaseExpiresAt,
3535
3546
  $updated: startedAt,
3536
3547
  $now: startedAt
@@ -3539,7 +3550,7 @@ class Store {
3539
3550
  if (res3.changes !== 1)
3540
3551
  return;
3541
3552
  const run3 = this.getRun(existing.id);
3542
- return run3 ? { run: run3, loop } : undefined;
3553
+ return run3 ? { run: run3, loop, claimToken } : undefined;
3543
3554
  }
3544
3555
  if (existing.status === "succeeded" || existing.status === "skipped") {
3545
3556
  this.db.exec("COMMIT");
@@ -3547,7 +3558,7 @@ class Store {
3547
3558
  }
3548
3559
  const attempt = existing.attempt + 1;
3549
3560
  const res2 = this.db.query(`UPDATE loop_runs SET attempt=$attempt, status='running', started_at=$started, finished_at=NULL,
3550
- claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3561
+ claimed_by=$claimedBy, claim_token=$claimToken, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
3551
3562
  duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
3552
3563
  WHERE id=$id
3553
3564
  AND status IN ('failed', 'timed_out', 'abandoned')
@@ -3556,6 +3567,7 @@ class Store {
3556
3567
  $attempt: attempt,
3557
3568
  $started: startedAt,
3558
3569
  $claimedBy: runnerId,
3570
+ $claimToken: claimToken,
3559
3571
  $lease: leaseExpiresAt,
3560
3572
  $updated: startedAt,
3561
3573
  $maxAttempts: loop.maxAttempts
@@ -3564,12 +3576,12 @@ class Store {
3564
3576
  if (res2.changes !== 1)
3565
3577
  return;
3566
3578
  const run2 = this.getRun(existing.id);
3567
- return run2 ? { run: run2, loop } : undefined;
3579
+ return run2 ? { run: run2, loop, claimToken } : undefined;
3568
3580
  }
3569
3581
  const id = genId();
3570
3582
  const res = this.db.query(`INSERT OR IGNORE INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
3571
- claimed_by, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
3572
- VALUES ($id, $loopId, $loopName, $scheduledFor, 1, 'running', $started, NULL, $claimedBy, $lease,
3583
+ claimed_by, claim_token, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
3584
+ VALUES ($id, $loopId, $loopName, $scheduledFor, 1, 'running', $started, NULL, $claimedBy, $claimToken, $lease,
3573
3585
  NULL, NULL, NULL, NULL, NULL, NULL, $created, $updated)`).run({
3574
3586
  $id: id,
3575
3587
  $loopId: loop.id,
@@ -3577,6 +3589,7 @@ class Store {
3577
3589
  $scheduledFor: scheduledFor,
3578
3590
  $started: startedAt,
3579
3591
  $claimedBy: runnerId,
3592
+ $claimToken: claimToken,
3580
3593
  $lease: leaseExpiresAt,
3581
3594
  $created: startedAt,
3582
3595
  $updated: startedAt
@@ -3585,7 +3598,7 @@ class Store {
3585
3598
  if (res.changes !== 1)
3586
3599
  return;
3587
3600
  const run = this.getRun(id);
3588
- return run ? { run, loop } : undefined;
3601
+ return run ? { run, loop, claimToken } : undefined;
3589
3602
  } catch (error) {
3590
3603
  try {
3591
3604
  this.db.exec("ROLLBACK");
@@ -3608,16 +3621,18 @@ class Store {
3608
3621
  $error: error ?? null,
3609
3622
  $updated: finishedAt,
3610
3623
  $claimedBy: opts.claimedBy ?? null,
3624
+ $claimToken: opts.claimToken ?? null,
3611
3625
  $now: (opts.now ?? new Date).toISOString(),
3612
3626
  $daemonLeaseId: opts.daemonLeaseId ?? null
3613
3627
  };
3614
3628
  return this.transact(() => {
3615
- 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,
3629
+ 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,
3616
3630
  duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3617
3631
  WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
3632
+ AND ($claimToken IS NULL OR claim_token=$claimToken)
3618
3633
  AND ($daemonLeaseId IS NULL OR EXISTS (
3619
3634
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3620
- ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3635
+ ))`).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,
3621
3636
  duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3622
3637
  const run = this.getRun(id);
3623
3638
  if (!run)
@@ -3645,11 +3660,13 @@ class Store {
3645
3660
  const expiresAt = new Date(now.getTime() + leaseMs).toISOString();
3646
3661
  const res = this.db.query(`UPDATE loop_runs SET lease_expires_at=$expires, updated_at=$updated
3647
3662
  WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
3663
+ AND ($claimToken IS NULL OR claim_token=$claimToken)
3648
3664
  AND ($daemonLeaseId IS NULL OR EXISTS (
3649
3665
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3650
3666
  ))`).run({
3651
3667
  $id: id,
3652
3668
  $claimedBy: claimedBy,
3669
+ $claimToken: opts.claimToken ?? null,
3653
3670
  $expires: expiresAt,
3654
3671
  $updated: now.toISOString(),
3655
3672
  $now: now.toISOString(),
@@ -45,24 +45,26 @@ export declare function verifierIdleTimeoutMs(input: {
45
45
  export declare function verifierRuntimeGuidance(input: {
46
46
  verifierIdleTimeoutMs?: number;
47
47
  }): string;
48
- export declare function todosInspectLine(todosProjectPath: string, taskId: string): string;
49
- export declare function todosStartLine(todosProjectPath: string, taskId: string): string;
50
- export declare function todosEvidenceLine(todosProjectPath: string, taskId: string, placeholder: string): string;
51
- export declare function todosVerificationLine(todosProjectPath: string, taskId: string): string;
52
- export declare function todosDoneLine(todosProjectPath: string, taskId: string): string;
48
+ export declare function todosInspectLine(todosProjectPath: string, taskId: string, todosDbPath?: string): string;
49
+ export declare function todosStartLine(todosProjectPath: string, taskId: string, todosDbPath?: string): string;
50
+ export declare function todosEvidenceLine(todosProjectPath: string, taskId: string, placeholder: string, todosDbPath?: string): string;
51
+ export declare function todosVerificationLine(todosProjectPath: string, taskId: string, todosDbPath?: string): string;
52
+ export declare function todosDoneLine(todosProjectPath: string, taskId: string, todosDbPath?: string): string;
53
53
  /** Exact-todos-commands stanza: project pin, cwd-inference warning, inspect, then role-specific command lines. */
54
- export declare function todosExactCommandsFragment(todosProjectPath: string, taskId: string, commandLines: string[]): string[];
54
+ export declare function todosExactCommandsFragment(todosProjectPath: string, taskId: string, commandLines: string[], todosDbPath?: string): string[];
55
55
  /** Worktree policy PROSE for agent guidance; enforcement is executor-native via target.worktree. */
56
56
  export declare function worktreePrompt(plan: AgentWorktreeSpec): string;
57
57
  export declare function worktreeContextFragment(plan: AgentWorktreeSpec): Record<string, unknown>;
58
58
  export declare function shellQuote(value: string): string;
59
- export declare function sourceTaskGateCommand(todosProjectPath: string, taskId: string): string;
59
+ export declare function todosEnvPrefix(todosDbPath?: string): string;
60
+ export declare function sourceTaskGateCommand(todosProjectPath: string, taskId: string, todosDbPath?: string): string;
60
61
  export type LifecycleGateStage = "triage" | "planner";
61
- export declare function lifecycleGateCommand(todosProjectPath: string, taskId: string, stage: LifecycleGateStage, goMarker: string, blockedMarker: string): string;
62
+ export declare function lifecycleGateCommand(todosProjectPath: string, taskId: string, stage: LifecycleGateStage, goMarker: string, blockedMarker: string, todosDbPath?: string): string;
62
63
  export interface PrHandoffCommandOptions {
63
64
  artifactPath: string;
64
65
  taskId: string;
65
66
  todosProjectPath: string;
67
+ todosDbPath?: string;
66
68
  worktreeCwd: string;
67
69
  worktreeRoot: string;
68
70
  expectedBranch: string;
@@ -38,6 +38,7 @@ export interface TodosTaskWorkflowTemplateInput extends AgentWorkflowTemplateBas
38
38
  taskTitle?: string;
39
39
  taskDescription?: string;
40
40
  todosProjectPath?: string;
41
+ todosDbPath?: string;
41
42
  prHandoff?: boolean;
42
43
  eventId?: string;
43
44
  eventType?: string;
package/dist/mcp/index.js CHANGED
@@ -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"]);
@@ -1525,7 +1525,7 @@ class Store {
1525
1525
  }
1526
1526
  const applied = new Set(this.db.query("SELECT id FROM schema_migrations").all().map((row) => row.id));
1527
1527
  for (const migration of this.migrations()) {
1528
- if (!migration.baseline && applied.has(migration.id))
1528
+ if (!migration.baseline && !migration.reapply && applied.has(migration.id))
1529
1529
  continue;
1530
1530
  migration.apply();
1531
1531
  if (!applied.has(migration.id)) {
@@ -1575,6 +1575,14 @@ 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
+ reapply: true,
1582
+ apply: () => {
1583
+ this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
1584
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
1585
+ }
1578
1586
  }
1579
1587
  ];
1580
1588
  }
@@ -1616,6 +1624,7 @@ class Store {
1616
1624
  started_at TEXT,
1617
1625
  finished_at TEXT,
1618
1626
  claimed_by TEXT,
1627
+ claim_token TEXT,
1619
1628
  lease_expires_at TEXT,
1620
1629
  pid INTEGER,
1621
1630
  pgid INTEGER,
@@ -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(),
@@ -5547,11 +5564,12 @@ function publicLoop(loop) {
5547
5564
  target
5548
5565
  };
5549
5566
  }
5550
- function publicRun(run, showOutput = false) {
5567
+ function publicRun(run, showOutput = false, opts = {}) {
5551
5568
  return {
5552
5569
  ...run,
5553
5570
  stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
5554
- stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
5571
+ stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
5572
+ error: opts.redactError ? redact(run.error) : run.error
5555
5573
  };
5556
5574
  }
5557
5575
  function publicExecutorResult(result, showOutput = false) {
@@ -7328,7 +7346,7 @@ async function tick(deps) {
7328
7346
  // package.json
7329
7347
  var package_default = {
7330
7348
  name: "@hasna/loops",
7331
- version: "0.4.1",
7349
+ version: "0.4.3",
7332
7350
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7333
7351
  type: "module",
7334
7352
  main: "dist/index.js",
@@ -7368,6 +7386,22 @@ var package_default = {
7368
7386
  "./storage": {
7369
7387
  types: "./dist/lib/store.d.ts",
7370
7388
  import: "./dist/lib/store.js"
7389
+ },
7390
+ "./storage/contract": {
7391
+ types: "./dist/lib/storage/contract.d.ts",
7392
+ import: "./dist/lib/storage/contract.js"
7393
+ },
7394
+ "./storage/sqlite": {
7395
+ types: "./dist/lib/storage/sqlite.d.ts",
7396
+ import: "./dist/lib/storage/sqlite.js"
7397
+ },
7398
+ "./storage/postgres": {
7399
+ types: "./dist/lib/storage/postgres.d.ts",
7400
+ import: "./dist/lib/storage/postgres.js"
7401
+ },
7402
+ "./storage/postgres-schema": {
7403
+ types: "./dist/lib/storage/postgres-schema.d.ts",
7404
+ import: "./dist/lib/storage/postgres-schema.js"
7371
7405
  }
7372
7406
  },
7373
7407
  files: [
@@ -7378,7 +7412,7 @@ var package_default = {
7378
7412
  "LICENSE"
7379
7413
  ],
7380
7414
  scripts: {
7381
- 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",
7415
+ 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",
7382
7416
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
7383
7417
  typecheck: "tsc --noEmit",
7384
7418
  test: "bun test",
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import type { ExecutorResult, Loop, LoopRun } from "../types.js";
2
3
  export declare function runnerStatus(machineId?: string | undefined): {
3
4
  ok: boolean;
4
5
  service: string;
@@ -6,4 +7,26 @@ export declare function runnerStatus(machineId?: string | undefined): {
6
7
  deployment: import("../index.js").LoopDeploymentStatus;
7
8
  state: string;
8
9
  };
10
+ export interface RunnerApiClaim {
11
+ loop: Loop;
12
+ run: LoopRun;
13
+ claimToken: string;
14
+ }
15
+ export interface RunnerOnceResult {
16
+ ok: boolean;
17
+ claimed: number;
18
+ completed: LoopRun[];
19
+ }
20
+ export interface RunRunnerOnceOptions {
21
+ apiUrl?: string;
22
+ apiToken?: string;
23
+ runnerId?: string;
24
+ machineId?: string;
25
+ now?: Date;
26
+ heartbeatIntervalMs?: number;
27
+ fetchImpl?: typeof fetch;
28
+ execute?: (loop: Loop, run: LoopRun) => Promise<ExecutorResult>;
29
+ env?: NodeJS.ProcessEnv;
30
+ }
31
+ export declare function runRunnerOnce(opts?: RunRunnerOnceOptions): Promise<RunnerOnceResult>;
9
32
  export declare function main(argv?: string[]): Promise<void>;