@hasna/loops 0.4.0 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +75 -33
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.js +1290 -0
- package/dist/cli/index.js +738 -201
- package/dist/daemon/index.js +64 -15
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1005 -155
- package/dist/lib/format.d.ts +3 -1
- package/dist/lib/mode.d.ts +48 -0
- package/dist/lib/mode.js +276 -0
- 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 +27 -10
- package/dist/lib/template-kit.d.ts +10 -8
- package/dist/lib/templates.d.ts +1 -0
- package/dist/mcp/index.js +64 -15
- package/dist/runner/index.d.ts +32 -0
- package/dist/runner/index.js +2037 -0
- package/dist/sdk/index.js +33 -15
- package/dist/types.d.ts +1 -1
- package/docs/DEPLOYMENT_MODES.md +148 -0
- package/docs/USAGE.md +66 -32
- package/package.json +34 -3
package/dist/daemon/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"]);
|
|
@@ -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.
|
|
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",
|
|
@@ -7613,6 +7631,8 @@ var package_default = {
|
|
|
7613
7631
|
bin: {
|
|
7614
7632
|
loops: "dist/cli/index.js",
|
|
7615
7633
|
"loops-daemon": "dist/daemon/index.js",
|
|
7634
|
+
"loops-api": "dist/api/index.js",
|
|
7635
|
+
"loops-runner": "dist/runner/index.js",
|
|
7616
7636
|
"loops-mcp": "dist/mcp/index.js"
|
|
7617
7637
|
},
|
|
7618
7638
|
exports: {
|
|
@@ -7628,9 +7648,37 @@ var package_default = {
|
|
|
7628
7648
|
types: "./dist/mcp/index.d.ts",
|
|
7629
7649
|
import: "./dist/mcp/index.js"
|
|
7630
7650
|
},
|
|
7651
|
+
"./api": {
|
|
7652
|
+
types: "./dist/api/index.d.ts",
|
|
7653
|
+
import: "./dist/api/index.js"
|
|
7654
|
+
},
|
|
7655
|
+
"./runner": {
|
|
7656
|
+
types: "./dist/runner/index.d.ts",
|
|
7657
|
+
import: "./dist/runner/index.js"
|
|
7658
|
+
},
|
|
7659
|
+
"./mode": {
|
|
7660
|
+
types: "./dist/lib/mode.d.ts",
|
|
7661
|
+
import: "./dist/lib/mode.js"
|
|
7662
|
+
},
|
|
7631
7663
|
"./storage": {
|
|
7632
7664
|
types: "./dist/lib/store.d.ts",
|
|
7633
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"
|
|
7634
7682
|
}
|
|
7635
7683
|
},
|
|
7636
7684
|
files: [
|
|
@@ -7641,14 +7689,15 @@ var package_default = {
|
|
|
7641
7689
|
"LICENSE"
|
|
7642
7690
|
],
|
|
7643
7691
|
scripts: {
|
|
7644
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/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",
|
|
7645
7693
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
7646
7694
|
typecheck: "tsc --noEmit",
|
|
7647
7695
|
test: "bun test",
|
|
7696
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
7648
7697
|
prepare: "test -d dist || bun run build",
|
|
7649
7698
|
"dev:cli": "bun run src/cli/index.ts",
|
|
7650
7699
|
"dev:daemon": "bun run src/daemon/index.ts",
|
|
7651
|
-
prepublishOnly: "bun run build"
|
|
7700
|
+
prepublishOnly: "bun run build && bun run test:boundary"
|
|
7652
7701
|
},
|
|
7653
7702
|
keywords: [
|
|
7654
7703
|
"loops",
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ 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";
|
|
25
|
+
export { LOOP_DEPLOYMENT_MODES, buildDeploymentStatus, deploymentStatusLine, loopControlPlaneConfig, normalizeLoopDeploymentMode, resolveLoopDeploymentMode, } from "./lib/mode.js";
|
|
26
|
+
export type { LoopControlPlaneConfig, LoopDeploymentMode, LoopDeploymentStatus, LoopModeResolution, LoopSourceOfTruth } from "./lib/mode.js";
|
|
23
27
|
export { executeLoop, executeTarget, preflightTarget } from "./lib/executor.js";
|
|
24
28
|
export { executeLoopTarget, executeWorkflow, preflightWorkflow } from "./lib/workflow-runner.js";
|
|
25
29
|
export { workflowBodyFromJson, workflowExecutionOrder } from "./lib/workflow-spec.js";
|