@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/sdk/index.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(),
|
|
@@ -5544,11 +5561,12 @@ function publicLoop(loop) {
|
|
|
5544
5561
|
target
|
|
5545
5562
|
};
|
|
5546
5563
|
}
|
|
5547
|
-
function publicRun(run, showOutput = false) {
|
|
5564
|
+
function publicRun(run, showOutput = false, opts = {}) {
|
|
5548
5565
|
return {
|
|
5549
5566
|
...run,
|
|
5550
5567
|
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
5551
|
-
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
5568
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
5569
|
+
error: opts.redactError ? redact(run.error) : run.error
|
|
5552
5570
|
};
|
|
5553
5571
|
}
|
|
5554
5572
|
function publicExecutorResult(result, showOutput = false) {
|
package/docs/DEPLOYMENT_MODES.md
CHANGED
|
@@ -79,12 +79,19 @@ JSON uses these field names:
|
|
|
79
79
|
|
|
80
80
|
`loops-api` is a separate process in the same public package. It is not a
|
|
81
81
|
separate package at this stage because self-hosted users and the hosted service
|
|
82
|
-
must share the same public contract.
|
|
82
|
+
must share the same public contract. The API server can expose storage-backed
|
|
83
|
+
`/v1` loop CRUD and run listing when an embedding host injects the public
|
|
84
|
+
storage contract. The standalone `loops-api serve` binary still fails closed
|
|
85
|
+
for those routes until a self-hosted storage adapter is wired by the operator or
|
|
86
|
+
platform host.
|
|
83
87
|
|
|
84
88
|
`loops-runner` is the process that connects a machine to a non-local control
|
|
85
|
-
plane. The
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
plane. The current public package supports a bounded one-shot protocol:
|
|
90
|
+
registration, claim polling, claim-token fenced heartbeat/finalization, and
|
|
91
|
+
`loops-runner run-once` execution for non-workflow targets. Full fleet daemon
|
|
92
|
+
mode, workflow target execution over the remote protocol, and import/export
|
|
93
|
+
migration still need follow-up releases before `loops-runner` is advertised as
|
|
94
|
+
a complete always-on worker.
|
|
88
95
|
|
|
89
96
|
## Machine Placement
|
|
90
97
|
|
|
@@ -109,9 +116,10 @@ mode.
|
|
|
109
116
|
|
|
110
117
|
Non-local execution needs these follow-up releases before it is live:
|
|
111
118
|
|
|
112
|
-
- Postgres
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
- A full Postgres control-plane adapter behind the public storage contract.
|
|
120
|
+
- Long-running runner daemon mode with backoff, fleet observability, and
|
|
121
|
+
durable machine registration records.
|
|
122
|
+
- Workflow target execution over the remote protocol.
|
|
115
123
|
- Import/export migration between local SQLite state and a control plane.
|
|
116
124
|
- Hosted product integration outside the public package.
|
|
117
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/loops",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,6 +40,22 @@
|
|
|
40
40
|
"./storage": {
|
|
41
41
|
"types": "./dist/lib/store.d.ts",
|
|
42
42
|
"import": "./dist/lib/store.js"
|
|
43
|
+
},
|
|
44
|
+
"./storage/contract": {
|
|
45
|
+
"types": "./dist/lib/storage/contract.d.ts",
|
|
46
|
+
"import": "./dist/lib/storage/contract.js"
|
|
47
|
+
},
|
|
48
|
+
"./storage/sqlite": {
|
|
49
|
+
"types": "./dist/lib/storage/sqlite.d.ts",
|
|
50
|
+
"import": "./dist/lib/storage/sqlite.js"
|
|
51
|
+
},
|
|
52
|
+
"./storage/postgres": {
|
|
53
|
+
"types": "./dist/lib/storage/postgres.d.ts",
|
|
54
|
+
"import": "./dist/lib/storage/postgres.js"
|
|
55
|
+
},
|
|
56
|
+
"./storage/postgres-schema": {
|
|
57
|
+
"types": "./dist/lib/storage/postgres-schema.d.ts",
|
|
58
|
+
"import": "./dist/lib/storage/postgres-schema.js"
|
|
43
59
|
}
|
|
44
60
|
},
|
|
45
61
|
"files": [
|
|
@@ -50,7 +66,7 @@
|
|
|
50
66
|
"LICENSE"
|
|
51
67
|
],
|
|
52
68
|
"scripts": {
|
|
53
|
-
"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",
|
|
69
|
+
"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",
|
|
54
70
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
55
71
|
"typecheck": "tsc --noEmit",
|
|
56
72
|
"test": "bun test",
|