@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.
- package/README.md +1 -1
- package/dist/api/index.d.ts +8 -2
- package/dist/api/index.js +961 -4
- package/dist/cli/index.js +459 -97
- package/dist/daemon/index.js +49 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +741 -53
- package/dist/lib/format.d.ts +3 -1
- package/dist/lib/mode.js +18 -2
- package/dist/lib/route/todos-cli.d.ts +1 -0
- package/dist/lib/route/types.d.ts +10 -0
- package/dist/lib/storage/contract.d.ts +133 -0
- package/dist/lib/storage/contract.js +1 -0
- package/dist/lib/storage/index.d.ts +6 -0
- package/dist/lib/storage/index.js +4612 -0
- package/dist/lib/storage/postgres-schema.d.ts +4 -0
- package/dist/lib/storage/postgres-schema.js +328 -0
- package/dist/lib/storage/postgres.d.ts +22 -0
- package/dist/lib/storage/postgres.js +423 -0
- package/dist/lib/storage/sqlite.d.ts +84 -0
- package/dist/lib/storage/sqlite.js +4187 -0
- package/dist/lib/store.d.ts +3 -0
- package/dist/lib/store.js +28 -11
- package/dist/lib/template-kit.d.ts +10 -8
- package/dist/lib/templates.d.ts +1 -0
- package/dist/mcp/index.js +49 -15
- package/dist/runner/index.d.ts +23 -0
- package/dist/runner/index.js +1746 -4
- package/dist/sdk/index.js +31 -13
- package/docs/DEPLOYMENT_MODES.md +15 -7
- package/package.json +18 -2
package/dist/lib/store.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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;
|
package/dist/lib/templates.d.ts
CHANGED
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 =
|
|
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.
|
|
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",
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -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>;
|