@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/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(),
|
|
@@ -3965,7 +3982,7 @@ class Store {
|
|
|
3965
3982
|
// package.json
|
|
3966
3983
|
var package_default = {
|
|
3967
3984
|
name: "@hasna/loops",
|
|
3968
|
-
version: "0.4.
|
|
3985
|
+
version: "0.4.3",
|
|
3969
3986
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
3970
3987
|
type: "module",
|
|
3971
3988
|
main: "dist/index.js",
|
|
@@ -4005,6 +4022,22 @@ var package_default = {
|
|
|
4005
4022
|
"./storage": {
|
|
4006
4023
|
types: "./dist/lib/store.d.ts",
|
|
4007
4024
|
import: "./dist/lib/store.js"
|
|
4025
|
+
},
|
|
4026
|
+
"./storage/contract": {
|
|
4027
|
+
types: "./dist/lib/storage/contract.d.ts",
|
|
4028
|
+
import: "./dist/lib/storage/contract.js"
|
|
4029
|
+
},
|
|
4030
|
+
"./storage/sqlite": {
|
|
4031
|
+
types: "./dist/lib/storage/sqlite.d.ts",
|
|
4032
|
+
import: "./dist/lib/storage/sqlite.js"
|
|
4033
|
+
},
|
|
4034
|
+
"./storage/postgres": {
|
|
4035
|
+
types: "./dist/lib/storage/postgres.d.ts",
|
|
4036
|
+
import: "./dist/lib/storage/postgres.js"
|
|
4037
|
+
},
|
|
4038
|
+
"./storage/postgres-schema": {
|
|
4039
|
+
types: "./dist/lib/storage/postgres-schema.d.ts",
|
|
4040
|
+
import: "./dist/lib/storage/postgres-schema.js"
|
|
4008
4041
|
}
|
|
4009
4042
|
},
|
|
4010
4043
|
files: [
|
|
@@ -4015,7 +4048,7 @@ var package_default = {
|
|
|
4015
4048
|
"LICENSE"
|
|
4016
4049
|
],
|
|
4017
4050
|
scripts: {
|
|
4018
|
-
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",
|
|
4051
|
+
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",
|
|
4019
4052
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
4020
4053
|
typecheck: "tsc --noEmit",
|
|
4021
4054
|
test: "bun test",
|
|
@@ -5796,11 +5829,12 @@ function publicLoop(loop) {
|
|
|
5796
5829
|
target
|
|
5797
5830
|
};
|
|
5798
5831
|
}
|
|
5799
|
-
function publicRun(run, showOutput = false) {
|
|
5832
|
+
function publicRun(run, showOutput = false, opts = {}) {
|
|
5800
5833
|
return {
|
|
5801
5834
|
...run,
|
|
5802
5835
|
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
5803
|
-
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
5836
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
5837
|
+
error: opts.redactError ? redact(run.error) : run.error
|
|
5804
5838
|
};
|
|
5805
5839
|
}
|
|
5806
5840
|
function publicExecutorResult(result, showOutput = false) {
|
|
@@ -8319,6 +8353,627 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
8319
8353
|
nonGoals: overrides.nonGoals ?? defaults.nonGoals
|
|
8320
8354
|
};
|
|
8321
8355
|
}
|
|
8356
|
+
|
|
8357
|
+
// src/lib/storage/sqlite.ts
|
|
8358
|
+
class SqliteLoopStorage {
|
|
8359
|
+
store;
|
|
8360
|
+
backend = "sqlite";
|
|
8361
|
+
supportsRemoteRunners = false;
|
|
8362
|
+
constructor(store = new Store) {
|
|
8363
|
+
this.store = store;
|
|
8364
|
+
}
|
|
8365
|
+
async close() {
|
|
8366
|
+
this.store.close();
|
|
8367
|
+
}
|
|
8368
|
+
async call(method, ...args) {
|
|
8369
|
+
return this.store[method](...args);
|
|
8370
|
+
}
|
|
8371
|
+
createLoop(...args) {
|
|
8372
|
+
return this.call("createLoop", ...args);
|
|
8373
|
+
}
|
|
8374
|
+
getLoop(...args) {
|
|
8375
|
+
return this.call("getLoop", ...args);
|
|
8376
|
+
}
|
|
8377
|
+
findLoopByName(...args) {
|
|
8378
|
+
return this.call("findLoopByName", ...args);
|
|
8379
|
+
}
|
|
8380
|
+
requireLoop(...args) {
|
|
8381
|
+
return this.call("requireLoop", ...args);
|
|
8382
|
+
}
|
|
8383
|
+
listLoops(...args) {
|
|
8384
|
+
return this.call("listLoops", ...args);
|
|
8385
|
+
}
|
|
8386
|
+
dueLoops(...args) {
|
|
8387
|
+
return this.call("dueLoops", ...args);
|
|
8388
|
+
}
|
|
8389
|
+
updateLoop(...args) {
|
|
8390
|
+
return this.call("updateLoop", ...args);
|
|
8391
|
+
}
|
|
8392
|
+
renameLoop(...args) {
|
|
8393
|
+
return this.call("renameLoop", ...args);
|
|
8394
|
+
}
|
|
8395
|
+
archiveLoop(...args) {
|
|
8396
|
+
return this.call("archiveLoop", ...args);
|
|
8397
|
+
}
|
|
8398
|
+
unarchiveLoop(...args) {
|
|
8399
|
+
return this.call("unarchiveLoop", ...args);
|
|
8400
|
+
}
|
|
8401
|
+
deleteLoop(...args) {
|
|
8402
|
+
return this.call("deleteLoop", ...args);
|
|
8403
|
+
}
|
|
8404
|
+
createWorkflow(...args) {
|
|
8405
|
+
return this.call("createWorkflow", ...args);
|
|
8406
|
+
}
|
|
8407
|
+
getWorkflow(...args) {
|
|
8408
|
+
return this.call("getWorkflow", ...args);
|
|
8409
|
+
}
|
|
8410
|
+
listWorkflows(...args) {
|
|
8411
|
+
return this.call("listWorkflows", ...args);
|
|
8412
|
+
}
|
|
8413
|
+
countWorkflows(...args) {
|
|
8414
|
+
return this.call("countWorkflows", ...args);
|
|
8415
|
+
}
|
|
8416
|
+
archiveWorkflow(...args) {
|
|
8417
|
+
return this.call("archiveWorkflow", ...args);
|
|
8418
|
+
}
|
|
8419
|
+
createWorkflowInvocation(...args) {
|
|
8420
|
+
return this.call("createWorkflowInvocation", ...args);
|
|
8421
|
+
}
|
|
8422
|
+
getWorkflowInvocation(...args) {
|
|
8423
|
+
return this.call("getWorkflowInvocation", ...args);
|
|
8424
|
+
}
|
|
8425
|
+
listWorkflowInvocations(...args) {
|
|
8426
|
+
return this.call("listWorkflowInvocations", ...args);
|
|
8427
|
+
}
|
|
8428
|
+
upsertWorkflowWorkItem(...args) {
|
|
8429
|
+
return this.call("upsertWorkflowWorkItem", ...args);
|
|
8430
|
+
}
|
|
8431
|
+
getWorkflowWorkItem(...args) {
|
|
8432
|
+
return this.call("getWorkflowWorkItem", ...args);
|
|
8433
|
+
}
|
|
8434
|
+
listWorkflowWorkItems(...args) {
|
|
8435
|
+
return this.call("listWorkflowWorkItems", ...args);
|
|
8436
|
+
}
|
|
8437
|
+
countActiveWorkflowWorkItems(...args) {
|
|
8438
|
+
return this.call("countActiveWorkflowWorkItems", ...args);
|
|
8439
|
+
}
|
|
8440
|
+
admitWorkflowWorkItem(...args) {
|
|
8441
|
+
return this.call("admitWorkflowWorkItem", ...args);
|
|
8442
|
+
}
|
|
8443
|
+
createGoal(...args) {
|
|
8444
|
+
return this.call("createGoal", ...args);
|
|
8445
|
+
}
|
|
8446
|
+
getGoal(...args) {
|
|
8447
|
+
return this.call("getGoal", ...args);
|
|
8448
|
+
}
|
|
8449
|
+
listGoals(...args) {
|
|
8450
|
+
return this.call("listGoals", ...args);
|
|
8451
|
+
}
|
|
8452
|
+
createGoalPlanNodes(...args) {
|
|
8453
|
+
return this.call("createGoalPlanNodes", ...args);
|
|
8454
|
+
}
|
|
8455
|
+
listGoalPlanNodes(...args) {
|
|
8456
|
+
return this.call("listGoalPlanNodes", ...args);
|
|
8457
|
+
}
|
|
8458
|
+
updateGoalStatus(...args) {
|
|
8459
|
+
return this.call("updateGoalStatus", ...args);
|
|
8460
|
+
}
|
|
8461
|
+
updateGoalPlanNode(...args) {
|
|
8462
|
+
return this.call("updateGoalPlanNode", ...args);
|
|
8463
|
+
}
|
|
8464
|
+
recordGoalEvent(...args) {
|
|
8465
|
+
return this.call("recordGoalEvent", ...args);
|
|
8466
|
+
}
|
|
8467
|
+
listGoalRuns(...args) {
|
|
8468
|
+
return this.call("listGoalRuns", ...args);
|
|
8469
|
+
}
|
|
8470
|
+
createWorkflowRun(...args) {
|
|
8471
|
+
return this.call("createWorkflowRun", ...args);
|
|
8472
|
+
}
|
|
8473
|
+
getWorkflowRun(...args) {
|
|
8474
|
+
return this.call("getWorkflowRun", ...args);
|
|
8475
|
+
}
|
|
8476
|
+
listWorkflowRuns(...args) {
|
|
8477
|
+
return this.call("listWorkflowRuns", ...args);
|
|
8478
|
+
}
|
|
8479
|
+
listWorkflowStepRuns(...args) {
|
|
8480
|
+
return this.call("listWorkflowStepRuns", ...args);
|
|
8481
|
+
}
|
|
8482
|
+
getWorkflowStepRun(...args) {
|
|
8483
|
+
return this.call("getWorkflowStepRun", ...args);
|
|
8484
|
+
}
|
|
8485
|
+
startWorkflowStepRun(...args) {
|
|
8486
|
+
return this.call("startWorkflowStepRun", ...args);
|
|
8487
|
+
}
|
|
8488
|
+
recoverWorkflowRun(...args) {
|
|
8489
|
+
return this.call("recoverWorkflowRun", ...args);
|
|
8490
|
+
}
|
|
8491
|
+
finalizeWorkflowStepRun(...args) {
|
|
8492
|
+
return this.call("finalizeWorkflowStepRun", ...args);
|
|
8493
|
+
}
|
|
8494
|
+
finalizeWorkflowRun(...args) {
|
|
8495
|
+
return this.call("finalizeWorkflowRun", ...args);
|
|
8496
|
+
}
|
|
8497
|
+
appendWorkflowEvent(...args) {
|
|
8498
|
+
return this.call("appendWorkflowEvent", ...args);
|
|
8499
|
+
}
|
|
8500
|
+
listWorkflowEvents(...args) {
|
|
8501
|
+
return this.call("listWorkflowEvents", ...args);
|
|
8502
|
+
}
|
|
8503
|
+
recordRunProcess(...args) {
|
|
8504
|
+
return this.call("recordRunProcess", ...args);
|
|
8505
|
+
}
|
|
8506
|
+
createSkippedRun(...args) {
|
|
8507
|
+
return this.call("createSkippedRun", ...args);
|
|
8508
|
+
}
|
|
8509
|
+
getRun(...args) {
|
|
8510
|
+
return this.call("getRun", ...args);
|
|
8511
|
+
}
|
|
8512
|
+
getRunBySlot(...args) {
|
|
8513
|
+
return this.call("getRunBySlot", ...args);
|
|
8514
|
+
}
|
|
8515
|
+
claimRun(...args) {
|
|
8516
|
+
return this.call("claimRun", ...args);
|
|
8517
|
+
}
|
|
8518
|
+
finalizeRun(...args) {
|
|
8519
|
+
return this.call("finalizeRun", ...args);
|
|
8520
|
+
}
|
|
8521
|
+
heartbeatRunLease(...args) {
|
|
8522
|
+
return this.call("heartbeatRunLease", ...args);
|
|
8523
|
+
}
|
|
8524
|
+
listRuns(...args) {
|
|
8525
|
+
return this.call("listRuns", ...args);
|
|
8526
|
+
}
|
|
8527
|
+
recoverExpiredRunLeases(...args) {
|
|
8528
|
+
return this.call("recoverExpiredRunLeases", ...args);
|
|
8529
|
+
}
|
|
8530
|
+
recoverExpiredRunLeasesDetailed(...args) {
|
|
8531
|
+
return this.call("recoverExpiredRunLeasesDetailed", ...args);
|
|
8532
|
+
}
|
|
8533
|
+
countLoops(...args) {
|
|
8534
|
+
return this.call("countLoops", ...args);
|
|
8535
|
+
}
|
|
8536
|
+
countRuns(...args) {
|
|
8537
|
+
return this.call("countRuns", ...args);
|
|
8538
|
+
}
|
|
8539
|
+
pruneHistory(...args) {
|
|
8540
|
+
return this.call("pruneHistory", ...args);
|
|
8541
|
+
}
|
|
8542
|
+
acquireDaemonLease(...args) {
|
|
8543
|
+
return this.call("acquireDaemonLease", ...args);
|
|
8544
|
+
}
|
|
8545
|
+
heartbeatDaemonLease(...args) {
|
|
8546
|
+
return this.call("heartbeatDaemonLease", ...args);
|
|
8547
|
+
}
|
|
8548
|
+
releaseDaemonLease(...args) {
|
|
8549
|
+
return this.call("releaseDaemonLease", ...args);
|
|
8550
|
+
}
|
|
8551
|
+
getDaemonLease(...args) {
|
|
8552
|
+
return this.call("getDaemonLease", ...args);
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
function createSqliteLoopStorage(path) {
|
|
8556
|
+
return new SqliteLoopStorage(new Store(path));
|
|
8557
|
+
}
|
|
8558
|
+
|
|
8559
|
+
// src/lib/storage/postgres-schema.ts
|
|
8560
|
+
import { createHash as createHash3 } from "crypto";
|
|
8561
|
+
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
8562
|
+
function checksumStorageSql(sql) {
|
|
8563
|
+
const normalized = sql.trim().replace(/\r\n/g, `
|
|
8564
|
+
`);
|
|
8565
|
+
return `sha256:${createHash3("sha256").update(normalized).digest("hex")}`;
|
|
8566
|
+
}
|
|
8567
|
+
function migration(id, sql) {
|
|
8568
|
+
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
8569
|
+
}
|
|
8570
|
+
var POSTGRES_STORAGE_MIGRATIONS = Object.freeze([
|
|
8571
|
+
migration("0001_core_runtime", `
|
|
8572
|
+
CREATE TABLE IF NOT EXISTS loops (
|
|
8573
|
+
id TEXT PRIMARY KEY,
|
|
8574
|
+
name TEXT NOT NULL,
|
|
8575
|
+
description TEXT,
|
|
8576
|
+
status TEXT NOT NULL,
|
|
8577
|
+
archived_at TIMESTAMPTZ,
|
|
8578
|
+
archived_from_status TEXT,
|
|
8579
|
+
schedule_json JSONB NOT NULL,
|
|
8580
|
+
target_json JSONB NOT NULL,
|
|
8581
|
+
goal_json JSONB,
|
|
8582
|
+
machine_json JSONB,
|
|
8583
|
+
next_run_at TIMESTAMPTZ,
|
|
8584
|
+
retry_scheduled_for TIMESTAMPTZ,
|
|
8585
|
+
catch_up TEXT NOT NULL,
|
|
8586
|
+
catch_up_limit INTEGER NOT NULL,
|
|
8587
|
+
overlap TEXT NOT NULL,
|
|
8588
|
+
max_attempts INTEGER NOT NULL,
|
|
8589
|
+
retry_delay_ms INTEGER NOT NULL,
|
|
8590
|
+
lease_ms INTEGER NOT NULL,
|
|
8591
|
+
expires_at TIMESTAMPTZ,
|
|
8592
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8593
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8594
|
+
);
|
|
8595
|
+
CREATE INDEX IF NOT EXISTS idx_loops_status_next ON loops(status, next_run_at);
|
|
8596
|
+
CREATE INDEX IF NOT EXISTS idx_loops_name ON loops(name);
|
|
8597
|
+
|
|
8598
|
+
CREATE TABLE IF NOT EXISTS loop_runs (
|
|
8599
|
+
id TEXT PRIMARY KEY,
|
|
8600
|
+
loop_id TEXT NOT NULL REFERENCES loops(id) ON DELETE CASCADE,
|
|
8601
|
+
loop_name TEXT NOT NULL,
|
|
8602
|
+
scheduled_for TIMESTAMPTZ NOT NULL,
|
|
8603
|
+
attempt INTEGER NOT NULL,
|
|
8604
|
+
status TEXT NOT NULL,
|
|
8605
|
+
started_at TIMESTAMPTZ,
|
|
8606
|
+
finished_at TIMESTAMPTZ,
|
|
8607
|
+
claimed_by TEXT,
|
|
8608
|
+
claim_token TEXT,
|
|
8609
|
+
lease_expires_at TIMESTAMPTZ,
|
|
8610
|
+
pid INTEGER,
|
|
8611
|
+
pgid INTEGER,
|
|
8612
|
+
process_started_at TIMESTAMPTZ,
|
|
8613
|
+
exit_code INTEGER,
|
|
8614
|
+
duration_ms INTEGER,
|
|
8615
|
+
stdout TEXT,
|
|
8616
|
+
stderr TEXT,
|
|
8617
|
+
error TEXT,
|
|
8618
|
+
goal_run_id TEXT,
|
|
8619
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8620
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8621
|
+
UNIQUE(loop_id, scheduled_for)
|
|
8622
|
+
);
|
|
8623
|
+
CREATE INDEX IF NOT EXISTS idx_runs_loop ON loop_runs(loop_id, created_at);
|
|
8624
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
8625
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
8626
|
+
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
8627
|
+
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
8628
|
+
|
|
8629
|
+
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
8630
|
+
id TEXT PRIMARY KEY,
|
|
8631
|
+
pid INTEGER NOT NULL,
|
|
8632
|
+
hostname TEXT NOT NULL,
|
|
8633
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
8634
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
8635
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8636
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8637
|
+
);
|
|
8638
|
+
`),
|
|
8639
|
+
migration("0002_workflows_goals", `
|
|
8640
|
+
CREATE TABLE IF NOT EXISTS workflow_specs (
|
|
8641
|
+
id TEXT PRIMARY KEY,
|
|
8642
|
+
name TEXT NOT NULL,
|
|
8643
|
+
description TEXT,
|
|
8644
|
+
version INTEGER NOT NULL,
|
|
8645
|
+
status TEXT NOT NULL,
|
|
8646
|
+
goal_json JSONB,
|
|
8647
|
+
steps_json JSONB NOT NULL,
|
|
8648
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8649
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8650
|
+
);
|
|
8651
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_status_name ON workflow_specs(status, name);
|
|
8652
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_name_active ON workflow_specs(name) WHERE status = 'active';
|
|
8653
|
+
|
|
8654
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
8655
|
+
id TEXT PRIMARY KEY,
|
|
8656
|
+
workflow_id TEXT NOT NULL REFERENCES workflow_specs(id) ON DELETE CASCADE,
|
|
8657
|
+
workflow_name TEXT NOT NULL,
|
|
8658
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
8659
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE SET NULL,
|
|
8660
|
+
invocation_id TEXT,
|
|
8661
|
+
work_item_id TEXT,
|
|
8662
|
+
scheduled_for TIMESTAMPTZ,
|
|
8663
|
+
idempotency_key TEXT,
|
|
8664
|
+
manifest_path TEXT,
|
|
8665
|
+
status TEXT NOT NULL,
|
|
8666
|
+
started_at TIMESTAMPTZ,
|
|
8667
|
+
finished_at TIMESTAMPTZ,
|
|
8668
|
+
duration_ms INTEGER,
|
|
8669
|
+
error TEXT,
|
|
8670
|
+
goal_run_id TEXT,
|
|
8671
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8672
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8673
|
+
);
|
|
8674
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_runs_idempotency ON workflow_runs(workflow_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
8675
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_created ON workflow_runs(workflow_id, created_at DESC);
|
|
8676
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_loop_run ON workflow_runs(loop_run_id);
|
|
8677
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
8678
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_invocation ON workflow_runs(invocation_id);
|
|
8679
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_work_item ON workflow_runs(work_item_id);
|
|
8680
|
+
|
|
8681
|
+
CREATE TABLE IF NOT EXISTS workflow_invocations (
|
|
8682
|
+
id TEXT PRIMARY KEY,
|
|
8683
|
+
workflow_id TEXT,
|
|
8684
|
+
template_id TEXT,
|
|
8685
|
+
source_kind TEXT NOT NULL,
|
|
8686
|
+
source_id TEXT,
|
|
8687
|
+
source_dedupe_key TEXT,
|
|
8688
|
+
source_json JSONB NOT NULL,
|
|
8689
|
+
subject_kind TEXT NOT NULL,
|
|
8690
|
+
subject_id TEXT,
|
|
8691
|
+
subject_path TEXT,
|
|
8692
|
+
subject_url TEXT,
|
|
8693
|
+
subject_json JSONB NOT NULL,
|
|
8694
|
+
intent TEXT NOT NULL,
|
|
8695
|
+
scope_json JSONB,
|
|
8696
|
+
output_policy_json JSONB,
|
|
8697
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8698
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8699
|
+
);
|
|
8700
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_source ON workflow_invocations(source_kind, source_id);
|
|
8701
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_subject ON workflow_invocations(subject_kind, subject_id, subject_path);
|
|
8702
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_invocations_dedupe ON workflow_invocations(source_kind, source_dedupe_key) WHERE source_dedupe_key IS NOT NULL;
|
|
8703
|
+
|
|
8704
|
+
CREATE TABLE IF NOT EXISTS workflow_work_items (
|
|
8705
|
+
id TEXT PRIMARY KEY,
|
|
8706
|
+
route_key TEXT NOT NULL,
|
|
8707
|
+
idempotency_key TEXT NOT NULL,
|
|
8708
|
+
invocation_id TEXT NOT NULL REFERENCES workflow_invocations(id) ON DELETE CASCADE,
|
|
8709
|
+
source_type TEXT NOT NULL,
|
|
8710
|
+
source_ref TEXT NOT NULL,
|
|
8711
|
+
subject_ref TEXT NOT NULL,
|
|
8712
|
+
project_key TEXT,
|
|
8713
|
+
project_group TEXT,
|
|
8714
|
+
priority INTEGER NOT NULL,
|
|
8715
|
+
status TEXT NOT NULL,
|
|
8716
|
+
attempts INTEGER NOT NULL,
|
|
8717
|
+
next_attempt_at TIMESTAMPTZ,
|
|
8718
|
+
lease_expires_at TIMESTAMPTZ,
|
|
8719
|
+
workflow_id TEXT REFERENCES workflow_specs(id) ON DELETE SET NULL,
|
|
8720
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
8721
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE SET NULL,
|
|
8722
|
+
last_reason TEXT,
|
|
8723
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8724
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8725
|
+
UNIQUE(route_key, idempotency_key)
|
|
8726
|
+
);
|
|
8727
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_status_next ON workflow_work_items(status, next_attempt_at, priority DESC, created_at ASC);
|
|
8728
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
8729
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
8730
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
8731
|
+
|
|
8732
|
+
CREATE TABLE IF NOT EXISTS workflow_step_runs (
|
|
8733
|
+
id TEXT PRIMARY KEY,
|
|
8734
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
8735
|
+
step_id TEXT NOT NULL,
|
|
8736
|
+
sequence INTEGER NOT NULL,
|
|
8737
|
+
status TEXT NOT NULL,
|
|
8738
|
+
started_at TIMESTAMPTZ,
|
|
8739
|
+
finished_at TIMESTAMPTZ,
|
|
8740
|
+
exit_code INTEGER,
|
|
8741
|
+
pid INTEGER,
|
|
8742
|
+
duration_ms INTEGER,
|
|
8743
|
+
stdout TEXT,
|
|
8744
|
+
stderr TEXT,
|
|
8745
|
+
error TEXT,
|
|
8746
|
+
account_profile TEXT,
|
|
8747
|
+
account_tool TEXT,
|
|
8748
|
+
goal_run_id TEXT,
|
|
8749
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8750
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8751
|
+
UNIQUE(workflow_run_id, step_id)
|
|
8752
|
+
);
|
|
8753
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_run_sequence ON workflow_step_runs(workflow_run_id, sequence);
|
|
8754
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_status ON workflow_step_runs(status);
|
|
8755
|
+
|
|
8756
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
8757
|
+
id TEXT PRIMARY KEY,
|
|
8758
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
8759
|
+
sequence INTEGER NOT NULL,
|
|
8760
|
+
event_type TEXT NOT NULL,
|
|
8761
|
+
step_id TEXT,
|
|
8762
|
+
payload_json JSONB,
|
|
8763
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8764
|
+
UNIQUE(workflow_run_id, sequence)
|
|
8765
|
+
);
|
|
8766
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
8767
|
+
|
|
8768
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
8769
|
+
id TEXT PRIMARY KEY,
|
|
8770
|
+
plan_id TEXT NOT NULL,
|
|
8771
|
+
objective TEXT NOT NULL,
|
|
8772
|
+
status TEXT NOT NULL,
|
|
8773
|
+
token_budget INTEGER,
|
|
8774
|
+
tokens_used INTEGER NOT NULL,
|
|
8775
|
+
time_used_seconds INTEGER NOT NULL,
|
|
8776
|
+
auto_execute TEXT NOT NULL,
|
|
8777
|
+
max_tokens INTEGER,
|
|
8778
|
+
source_type TEXT,
|
|
8779
|
+
source_id TEXT,
|
|
8780
|
+
loop_id TEXT,
|
|
8781
|
+
loop_run_id TEXT,
|
|
8782
|
+
workflow_id TEXT,
|
|
8783
|
+
workflow_run_id TEXT,
|
|
8784
|
+
workflow_step_id TEXT,
|
|
8785
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8786
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8787
|
+
);
|
|
8788
|
+
CREATE INDEX IF NOT EXISTS idx_goals_status_updated ON goals(status, updated_at DESC);
|
|
8789
|
+
CREATE INDEX IF NOT EXISTS idx_goals_loop_run ON goals(loop_run_id);
|
|
8790
|
+
CREATE INDEX IF NOT EXISTS idx_goals_workflow_run ON goals(workflow_run_id);
|
|
8791
|
+
CREATE INDEX IF NOT EXISTS idx_goals_source ON goals(source_type, source_id);
|
|
8792
|
+
|
|
8793
|
+
CREATE TABLE IF NOT EXISTS goal_plan_nodes (
|
|
8794
|
+
id TEXT PRIMARY KEY,
|
|
8795
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
8796
|
+
plan_id TEXT NOT NULL,
|
|
8797
|
+
key TEXT NOT NULL,
|
|
8798
|
+
sequence INTEGER NOT NULL,
|
|
8799
|
+
priority INTEGER NOT NULL,
|
|
8800
|
+
objective TEXT NOT NULL,
|
|
8801
|
+
status TEXT NOT NULL,
|
|
8802
|
+
ready BOOLEAN NOT NULL,
|
|
8803
|
+
token_budget INTEGER,
|
|
8804
|
+
tokens_used INTEGER NOT NULL,
|
|
8805
|
+
time_used_seconds INTEGER NOT NULL,
|
|
8806
|
+
depends_on_json JSONB NOT NULL,
|
|
8807
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8808
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8809
|
+
UNIQUE(plan_id, key)
|
|
8810
|
+
);
|
|
8811
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_goal_sequence ON goal_plan_nodes(goal_id, sequence);
|
|
8812
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_status ON goal_plan_nodes(status);
|
|
8813
|
+
|
|
8814
|
+
CREATE TABLE IF NOT EXISTS goal_runs (
|
|
8815
|
+
id TEXT PRIMARY KEY,
|
|
8816
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
8817
|
+
plan_id TEXT NOT NULL,
|
|
8818
|
+
loop_id TEXT,
|
|
8819
|
+
loop_run_id TEXT,
|
|
8820
|
+
workflow_id TEXT,
|
|
8821
|
+
workflow_run_id TEXT,
|
|
8822
|
+
workflow_step_id TEXT,
|
|
8823
|
+
turn INTEGER NOT NULL,
|
|
8824
|
+
phase TEXT NOT NULL,
|
|
8825
|
+
status TEXT NOT NULL,
|
|
8826
|
+
node_key TEXT,
|
|
8827
|
+
tokens_used INTEGER NOT NULL,
|
|
8828
|
+
evidence_json JSONB,
|
|
8829
|
+
raw_response_json JSONB,
|
|
8830
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8831
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8832
|
+
);
|
|
8833
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_goal_created ON goal_runs(goal_id, created_at);
|
|
8834
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_loop_run ON goal_runs(loop_run_id);
|
|
8835
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
8836
|
+
`),
|
|
8837
|
+
migration("0003_remote_runners_and_audit", `
|
|
8838
|
+
CREATE TABLE IF NOT EXISTS runner_machines (
|
|
8839
|
+
id TEXT PRIMARY KEY,
|
|
8840
|
+
hostname TEXT NOT NULL,
|
|
8841
|
+
labels_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
8842
|
+
capabilities_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
8843
|
+
status TEXT NOT NULL,
|
|
8844
|
+
last_seen_at TIMESTAMPTZ NOT NULL,
|
|
8845
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8846
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8847
|
+
);
|
|
8848
|
+
CREATE INDEX IF NOT EXISTS idx_runner_machines_status_seen ON runner_machines(status, last_seen_at DESC);
|
|
8849
|
+
|
|
8850
|
+
CREATE TABLE IF NOT EXISTS runner_leases (
|
|
8851
|
+
id TEXT PRIMARY KEY,
|
|
8852
|
+
runner_id TEXT NOT NULL REFERENCES runner_machines(id) ON DELETE CASCADE,
|
|
8853
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE CASCADE,
|
|
8854
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
8855
|
+
claim_token TEXT NOT NULL,
|
|
8856
|
+
status TEXT NOT NULL,
|
|
8857
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
8858
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
8859
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8860
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8861
|
+
CHECK (loop_run_id IS NOT NULL OR workflow_run_id IS NOT NULL)
|
|
8862
|
+
);
|
|
8863
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_runner_leases_active_loop_run ON runner_leases(loop_run_id) WHERE loop_run_id IS NOT NULL AND status = 'active';
|
|
8864
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_runner_leases_active_workflow_run ON runner_leases(workflow_run_id) WHERE workflow_run_id IS NOT NULL AND status = 'active';
|
|
8865
|
+
CREATE INDEX IF NOT EXISTS idx_runner_leases_runner_status ON runner_leases(runner_id, status, expires_at);
|
|
8866
|
+
CREATE INDEX IF NOT EXISTS idx_runner_leases_claim_token ON runner_leases(claim_token);
|
|
8867
|
+
|
|
8868
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
8869
|
+
id TEXT PRIMARY KEY,
|
|
8870
|
+
actor TEXT NOT NULL,
|
|
8871
|
+
action TEXT NOT NULL,
|
|
8872
|
+
subject_type TEXT NOT NULL,
|
|
8873
|
+
subject_id TEXT NOT NULL,
|
|
8874
|
+
metadata_json JSONB,
|
|
8875
|
+
created_at TIMESTAMPTZ NOT NULL
|
|
8876
|
+
);
|
|
8877
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_subject ON audit_events(subject_type, subject_id, created_at DESC);
|
|
8878
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action, created_at DESC);
|
|
8879
|
+
`)
|
|
8880
|
+
]);
|
|
8881
|
+
|
|
8882
|
+
// src/lib/storage/postgres.ts
|
|
8883
|
+
class PostgresStorage {
|
|
8884
|
+
executor;
|
|
8885
|
+
backend = "postgres";
|
|
8886
|
+
migrations;
|
|
8887
|
+
constructor(executor, migrations = POSTGRES_STORAGE_MIGRATIONS) {
|
|
8888
|
+
this.executor = executor;
|
|
8889
|
+
this.migrations = migrations;
|
|
8890
|
+
}
|
|
8891
|
+
async listAppliedMigrations() {
|
|
8892
|
+
await this.ensureLedger();
|
|
8893
|
+
return this.readAppliedMigrations();
|
|
8894
|
+
}
|
|
8895
|
+
async migrate(opts = {}) {
|
|
8896
|
+
const dryRun = opts.dryRun === true;
|
|
8897
|
+
if (!dryRun)
|
|
8898
|
+
await this.ensureLedger();
|
|
8899
|
+
const applied = dryRun ? await this.tryReadAppliedMigrations() : await this.readAppliedMigrations();
|
|
8900
|
+
const knownMigrationIds = new Set(this.migrations.map((migration2) => migration2.id));
|
|
8901
|
+
for (const row of applied) {
|
|
8902
|
+
if (!knownMigrationIds.has(row.id)) {
|
|
8903
|
+
throw new Error(`Postgres migration ${row.id} is not recognized by this binary`);
|
|
8904
|
+
}
|
|
8905
|
+
}
|
|
8906
|
+
const appliedById = new Map(applied.map((row) => [row.id, row]));
|
|
8907
|
+
const plan = this.migrations.map((migration2) => ({
|
|
8908
|
+
migration: migration2,
|
|
8909
|
+
state: appliedById.has(migration2.id) ? "already_applied" : "pending"
|
|
8910
|
+
}));
|
|
8911
|
+
for (const migration2 of this.migrations) {
|
|
8912
|
+
const existing = appliedById.get(migration2.id);
|
|
8913
|
+
if (existing && existing.checksum !== migration2.checksum) {
|
|
8914
|
+
throw new Error(`Postgres migration checksum mismatch for ${migration2.id}`);
|
|
8915
|
+
}
|
|
8916
|
+
}
|
|
8917
|
+
if (dryRun)
|
|
8918
|
+
return { backend: this.backend, dryRun, applied, plan };
|
|
8919
|
+
const run = async () => {
|
|
8920
|
+
for (const item of plan) {
|
|
8921
|
+
if (item.state === "already_applied")
|
|
8922
|
+
continue;
|
|
8923
|
+
await this.executor.execute(item.migration.sql);
|
|
8924
|
+
await this.executor.execute(`INSERT INTO ${POSTGRES_MIGRATION_LEDGER_TABLE} (id, checksum, applied_at) VALUES ($1, $2, NOW())`, [item.migration.id, item.migration.checksum]);
|
|
8925
|
+
}
|
|
8926
|
+
};
|
|
8927
|
+
if (this.executor.transaction)
|
|
8928
|
+
await this.executor.transaction(run);
|
|
8929
|
+
else
|
|
8930
|
+
await run();
|
|
8931
|
+
return {
|
|
8932
|
+
backend: this.backend,
|
|
8933
|
+
dryRun,
|
|
8934
|
+
applied: await this.readAppliedMigrations(),
|
|
8935
|
+
plan
|
|
8936
|
+
};
|
|
8937
|
+
}
|
|
8938
|
+
async close() {
|
|
8939
|
+
await this.executor.close?.();
|
|
8940
|
+
}
|
|
8941
|
+
async ensureLedger() {
|
|
8942
|
+
await this.executor.execute(`
|
|
8943
|
+
CREATE TABLE IF NOT EXISTS ${POSTGRES_MIGRATION_LEDGER_TABLE} (
|
|
8944
|
+
id TEXT PRIMARY KEY,
|
|
8945
|
+
checksum TEXT NOT NULL,
|
|
8946
|
+
applied_at TIMESTAMPTZ NOT NULL
|
|
8947
|
+
);
|
|
8948
|
+
`);
|
|
8949
|
+
}
|
|
8950
|
+
async readAppliedMigrations() {
|
|
8951
|
+
const rows = await this.executor.query(`SELECT id, checksum, applied_at FROM ${POSTGRES_MIGRATION_LEDGER_TABLE} ORDER BY id ASC`);
|
|
8952
|
+
return rows.map((row) => ({
|
|
8953
|
+
id: row.id,
|
|
8954
|
+
checksum: row.checksum,
|
|
8955
|
+
appliedAt: row.applied_at instanceof Date ? row.applied_at.toISOString() : row.applied_at
|
|
8956
|
+
}));
|
|
8957
|
+
}
|
|
8958
|
+
async tryReadAppliedMigrations() {
|
|
8959
|
+
try {
|
|
8960
|
+
return await this.readAppliedMigrations();
|
|
8961
|
+
} catch (error) {
|
|
8962
|
+
if (isMissingLedgerError(error))
|
|
8963
|
+
return [];
|
|
8964
|
+
throw error;
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
function createPostgresStorage(executor) {
|
|
8969
|
+
return new PostgresStorage(executor);
|
|
8970
|
+
}
|
|
8971
|
+
function isMissingLedgerError(error) {
|
|
8972
|
+
if (!(error instanceof Error))
|
|
8973
|
+
return false;
|
|
8974
|
+
const message = error.message.toLowerCase();
|
|
8975
|
+
return message.includes(POSTGRES_MIGRATION_LEDGER_TABLE.toLowerCase()) && (message.includes("does not exist") || message.includes("no such table") || message.includes("undefined_table"));
|
|
8976
|
+
}
|
|
8322
8977
|
// src/lib/templates.ts
|
|
8323
8978
|
import { execFileSync } from "child_process";
|
|
8324
8979
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -8403,6 +9058,7 @@ var BUILTIN_TEMPLATE_SUMMARIES = [
|
|
|
8403
9058
|
{ name: "taskTitle", description: "Human-readable task title." },
|
|
8404
9059
|
projectPathVariable(),
|
|
8405
9060
|
{ name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
|
|
9061
|
+
{ name: "todosDbPath", description: "Optional exact Todos DB path used via TODOS_DB_PATH/HASNA_TODOS_DB_PATH." },
|
|
8406
9062
|
...routeScopeVariables(),
|
|
8407
9063
|
...workerVerifierAgentVariables({ addDirs: true, branchNoun: "task/event" })
|
|
8408
9064
|
]
|
|
@@ -8443,6 +9099,8 @@ var BUILTIN_TEMPLATE_SUMMARIES = [
|
|
|
8443
9099
|
variables: [
|
|
8444
9100
|
{ name: "taskId", required: true, description: "Todos task id." },
|
|
8445
9101
|
projectPathVariable(),
|
|
9102
|
+
{ name: "todosProjectPath", description: "Todos storage project path used in lifecycle commands." },
|
|
9103
|
+
{ name: "todosDbPath", description: "Optional exact Todos DB path used via TODOS_DB_PATH/HASNA_TODOS_DB_PATH." },
|
|
8446
9104
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
8447
9105
|
roleAuthProfileVariable("triage"),
|
|
8448
9106
|
roleAuthProfileVariable("planner"),
|
|
@@ -8573,28 +9231,29 @@ function verifierRuntimeGuidance(input) {
|
|
|
8573
9231
|
].join(`
|
|
8574
9232
|
`);
|
|
8575
9233
|
}
|
|
8576
|
-
function todosInspectLine(todosProjectPath, taskId) {
|
|
8577
|
-
return `- Inspect first:
|
|
9234
|
+
function todosInspectLine(todosProjectPath, taskId, todosDbPath) {
|
|
9235
|
+
return `- Inspect first: ${todosCommand(todosProjectPath, todosDbPath)} inspect ${taskId}`;
|
|
8578
9236
|
}
|
|
8579
|
-
function todosStartLine(todosProjectPath, taskId) {
|
|
8580
|
-
return `- Claim/start if appropriate:
|
|
9237
|
+
function todosStartLine(todosProjectPath, taskId, todosDbPath) {
|
|
9238
|
+
return `- Claim/start if appropriate: ${todosCommand(todosProjectPath, todosDbPath)} start ${taskId}`;
|
|
8581
9239
|
}
|
|
8582
|
-
function todosEvidenceLine(todosProjectPath, taskId, placeholder) {
|
|
8583
|
-
return `- Record evidence:
|
|
9240
|
+
function todosEvidenceLine(todosProjectPath, taskId, placeholder, todosDbPath) {
|
|
9241
|
+
return `- Record evidence: ${todosCommand(todosProjectPath, todosDbPath)} comment ${taskId} "<${placeholder}>"`;
|
|
8584
9242
|
}
|
|
8585
|
-
function todosVerificationLine(todosProjectPath, taskId) {
|
|
8586
|
-
return `- Record verification:
|
|
9243
|
+
function todosVerificationLine(todosProjectPath, taskId, todosDbPath) {
|
|
9244
|
+
return `- Record verification: ${todosCommand(todosProjectPath, todosDbPath)} comment ${taskId} "<verification evidence or blocker>"`;
|
|
8587
9245
|
}
|
|
8588
|
-
function todosDoneLine(todosProjectPath, taskId) {
|
|
8589
|
-
return `- If valid and complete:
|
|
9246
|
+
function todosDoneLine(todosProjectPath, taskId, todosDbPath) {
|
|
9247
|
+
return `- If valid and complete: ${todosCommand(todosProjectPath, todosDbPath)} done ${taskId}`;
|
|
8590
9248
|
}
|
|
8591
|
-
function todosExactCommandsFragment(todosProjectPath, taskId, commandLines) {
|
|
9249
|
+
function todosExactCommandsFragment(todosProjectPath, taskId, commandLines, todosDbPath) {
|
|
8592
9250
|
return [
|
|
8593
9251
|
`Todos project path: ${todosProjectPath}`,
|
|
9252
|
+
todosDbPath ? `Todos DB path: ${todosDbPath}` : undefined,
|
|
8594
9253
|
"Use these exact todos commands so worktree cwd inference cannot attach to the wrong project:",
|
|
8595
|
-
todosInspectLine(todosProjectPath, taskId),
|
|
9254
|
+
todosInspectLine(todosProjectPath, taskId, todosDbPath),
|
|
8596
9255
|
...commandLines
|
|
8597
|
-
];
|
|
9256
|
+
].filter((line) => Boolean(line));
|
|
8598
9257
|
}
|
|
8599
9258
|
function worktreePrompt(plan) {
|
|
8600
9259
|
if (plan.enabled) {
|
|
@@ -8631,10 +9290,19 @@ function worktreeContextFragment(plan) {
|
|
|
8631
9290
|
function shellQuote2(value) {
|
|
8632
9291
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8633
9292
|
}
|
|
8634
|
-
function
|
|
9293
|
+
function todosEnvPrefix(todosDbPath) {
|
|
9294
|
+
return todosDbPath ? `TODOS_DB_PATH=${shellQuote2(todosDbPath)} HASNA_TODOS_DB_PATH=${shellQuote2(todosDbPath)} ` : "";
|
|
9295
|
+
}
|
|
9296
|
+
function todosCommand(todosProjectPath, todosDbPath) {
|
|
9297
|
+
return `${todosEnvPrefix(todosDbPath)}todos --project ${todosProjectPath}`;
|
|
9298
|
+
}
|
|
9299
|
+
function shellTodosCommand(todosProjectPath, todosDbPath) {
|
|
9300
|
+
return `${todosEnvPrefix(todosDbPath)}todos --project ${shellQuote2(todosProjectPath)}`;
|
|
9301
|
+
}
|
|
9302
|
+
function sourceTaskGateCommand(todosProjectPath, taskId, todosDbPath) {
|
|
8635
9303
|
return [
|
|
8636
9304
|
"set -euo pipefail",
|
|
8637
|
-
|
|
9305
|
+
`${shellTodosCommand(todosProjectPath, todosDbPath)} --json inspect ${shellQuote2(taskId)} >/dev/null`,
|
|
8638
9306
|
`printf "source task %s resolved in todos project %s\\n" ${shellQuote2(taskId)} ${shellQuote2(todosProjectPath)}`
|
|
8639
9307
|
].join(`
|
|
8640
9308
|
`);
|
|
@@ -8694,10 +9362,10 @@ var LIFECYCLE_GATE_SCRIPT_TAIL = [
|
|
|
8694
9362
|
"console.log(`task lifecycle ${stage} gate passed for ${task.id || task.taskId || 'task'} status=${status || 'unknown'}`);"
|
|
8695
9363
|
].join(`
|
|
8696
9364
|
`);
|
|
8697
|
-
function lifecycleGateCommand(todosProjectPath, taskId, stage, goMarker, blockedMarker) {
|
|
9365
|
+
function lifecycleGateCommand(todosProjectPath, taskId, stage, goMarker, blockedMarker, todosDbPath) {
|
|
8698
9366
|
return [
|
|
8699
9367
|
"set -euo pipefail",
|
|
8700
|
-
`task_json="$(
|
|
9368
|
+
`task_json="$(${shellTodosCommand(todosProjectPath, todosDbPath)} --json inspect ${shellQuote2(taskId)})"`,
|
|
8701
9369
|
`TASK_JSON="$task_json" STAGE=${shellQuote2(stage)} bun - <<'BUN'`,
|
|
8702
9370
|
LIFECYCLE_GATE_SCRIPT_HEAD,
|
|
8703
9371
|
`const goMarker = ${JSON.stringify(goMarker)};`,
|
|
@@ -8713,6 +9381,7 @@ var PR_HANDOFF_SCRIPT = [
|
|
|
8713
9381
|
"const artifactPath = process.env.OPENLOOPS_PR_HANDOFF_ARTIFACT || '';",
|
|
8714
9382
|
"const taskId = process.env.OPENLOOPS_PR_HANDOFF_TASK_ID || '';",
|
|
8715
9383
|
"const todosProject = process.env.OPENLOOPS_PR_HANDOFF_TODOS_PROJECT || '';",
|
|
9384
|
+
"const todosDbPath = process.env.OPENLOOPS_PR_HANDOFF_TODOS_DB_PATH || '';",
|
|
8716
9385
|
"const fallbackWorktree = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE || process.cwd();",
|
|
8717
9386
|
"const expectedRoot = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT || fallbackWorktree;",
|
|
8718
9387
|
"const expectedBranch = process.env.OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH || '';",
|
|
@@ -8730,7 +9399,8 @@ var PR_HANDOFF_SCRIPT = [
|
|
|
8730
9399
|
"};",
|
|
8731
9400
|
"const run = (command, args, options = {}) => spawnSync(command, args, { encoding: 'utf8', ...options });",
|
|
8732
9401
|
"const todosArgs = (...args) => todosProject ? ['--project', todosProject, ...args] : args;",
|
|
8733
|
-
"const
|
|
9402
|
+
"const todosEnv = todosDbPath ? { ...process.env, TODOS_DB_PATH: todosDbPath, HASNA_TODOS_DB_PATH: todosDbPath } : process.env;",
|
|
9403
|
+
"const todos = (...args) => run(todosBin, todosArgs(...args), { env: todosEnv });",
|
|
8734
9404
|
"const comment = (text) => {",
|
|
8735
9405
|
" const result = todos('comment', taskId, text);",
|
|
8736
9406
|
" if (result.status !== 0) console.error(`failed to comment original task: ${result.stderr || result.stdout || result.status}`);",
|
|
@@ -8859,6 +9529,7 @@ function prHandoffCommand(opts) {
|
|
|
8859
9529
|
`export OPENLOOPS_PR_HANDOFF_ARTIFACT=${shellQuote2(opts.artifactPath)}`,
|
|
8860
9530
|
`export OPENLOOPS_PR_HANDOFF_TASK_ID=${shellQuote2(opts.taskId)}`,
|
|
8861
9531
|
`export OPENLOOPS_PR_HANDOFF_TODOS_PROJECT=${shellQuote2(opts.todosProjectPath)}`,
|
|
9532
|
+
opts.todosDbPath ? `export OPENLOOPS_PR_HANDOFF_TODOS_DB_PATH=${shellQuote2(opts.todosDbPath)}` : undefined,
|
|
8862
9533
|
`export OPENLOOPS_PR_HANDOFF_WORKTREE=${shellQuote2(opts.worktreeCwd)}`,
|
|
8863
9534
|
`export OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT=${shellQuote2(opts.worktreeRoot)}`,
|
|
8864
9535
|
`export OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH=${shellQuote2(opts.expectedBranch)}`,
|
|
@@ -8869,7 +9540,7 @@ function prHandoffCommand(opts) {
|
|
|
8869
9540
|
"bun - <<'BUN'",
|
|
8870
9541
|
PR_HANDOFF_SCRIPT,
|
|
8871
9542
|
"BUN"
|
|
8872
|
-
].join(`
|
|
9543
|
+
].filter((line) => Boolean(line)).join(`
|
|
8873
9544
|
`);
|
|
8874
9545
|
}
|
|
8875
9546
|
// src/lib/templates-custom.ts
|
|
@@ -9459,12 +10130,12 @@ function commandStep(opts) {
|
|
|
9459
10130
|
...opts.blockedExitCodes ? { blockedExitCodes: opts.blockedExitCodes } : {}
|
|
9460
10131
|
};
|
|
9461
10132
|
}
|
|
9462
|
-
function sourceTaskGateStep(todosProjectPath, taskId, plan, description) {
|
|
10133
|
+
function sourceTaskGateStep(todosProjectPath, taskId, plan, description, todosDbPath) {
|
|
9463
10134
|
return commandStep({
|
|
9464
10135
|
id: "source-task-gate",
|
|
9465
10136
|
name: "Source Task Gate",
|
|
9466
10137
|
description,
|
|
9467
|
-
command: sourceTaskGateCommand(todosProjectPath, taskId),
|
|
10138
|
+
command: sourceTaskGateCommand(todosProjectPath, taskId, todosDbPath),
|
|
9468
10139
|
cwd: plan.originalCwd,
|
|
9469
10140
|
timeoutMs: 60000,
|
|
9470
10141
|
blockedExitCodes: GATE_BLOCKED_EXIT_CODES
|
|
@@ -9476,7 +10147,7 @@ function lifecycleGateStep(opts) {
|
|
|
9476
10147
|
name: opts.stage === "triage" ? "Triage Gate" : "Planner Gate",
|
|
9477
10148
|
description: opts.description,
|
|
9478
10149
|
dependsOn: opts.dependsOn,
|
|
9479
|
-
command: lifecycleGateCommand(opts.todosProjectPath, opts.taskId, opts.stage, opts.goMarker, opts.blockedMarker),
|
|
10150
|
+
command: lifecycleGateCommand(opts.todosProjectPath, opts.taskId, opts.stage, opts.goMarker, opts.blockedMarker, opts.todosDbPath),
|
|
9480
10151
|
cwd: opts.plan.originalCwd,
|
|
9481
10152
|
timeoutMs: 2 * 60000,
|
|
9482
10153
|
blockedExitCodes: GATE_BLOCKED_EXIT_CODES
|
|
@@ -9485,7 +10156,7 @@ function lifecycleGateStep(opts) {
|
|
|
9485
10156
|
function prHandoffArtifactPath(plan, taskId) {
|
|
9486
10157
|
return join7(plan.cwd, ".openloops", "pr-handoff", `${slugSegment(taskId, "task")}.json`);
|
|
9487
10158
|
}
|
|
9488
|
-
function prHandoffStep(input, plan, todosProjectPath) {
|
|
10159
|
+
function prHandoffStep(input, plan, todosProjectPath, todosDbPath) {
|
|
9489
10160
|
return commandStep({
|
|
9490
10161
|
id: "pr-handoff",
|
|
9491
10162
|
name: "PR Handoff",
|
|
@@ -9495,6 +10166,7 @@ function prHandoffStep(input, plan, todosProjectPath) {
|
|
|
9495
10166
|
artifactPath: prHandoffArtifactPath(plan, input.taskId),
|
|
9496
10167
|
taskId: input.taskId,
|
|
9497
10168
|
todosProjectPath,
|
|
10169
|
+
todosDbPath,
|
|
9498
10170
|
worktreeCwd: plan.cwd,
|
|
9499
10171
|
worktreeRoot: plan.path ?? plan.cwd,
|
|
9500
10172
|
expectedBranch: plan.branch ?? ""
|
|
@@ -9581,6 +10253,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9581
10253
|
if (!input.projectPath?.trim())
|
|
9582
10254
|
throw new Error("projectPath is required");
|
|
9583
10255
|
const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
|
|
10256
|
+
const todosDbPath = input.todosDbPath;
|
|
9584
10257
|
const plan = worktreePlan(input, input.taskId);
|
|
9585
10258
|
const taskContext = {
|
|
9586
10259
|
taskId: input.taskId,
|
|
@@ -9591,15 +10264,17 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9591
10264
|
projectPath: input.projectPath,
|
|
9592
10265
|
routeProjectPath: input.routeProjectPath,
|
|
9593
10266
|
projectGroup: input.projectGroup,
|
|
10267
|
+
todosProjectPath: input.todosProjectPath ?? (input.todosDbPath ? todosProjectPath : undefined),
|
|
10268
|
+
todosDbPath,
|
|
9594
10269
|
worktree: worktreeContextFragment(plan)
|
|
9595
10270
|
};
|
|
9596
10271
|
const workerPrompt = [
|
|
9597
10272
|
...goalHeaderFragment(`Complete todos task ${input.taskId} in ${input.projectPath}.`, "worker", "task"),
|
|
9598
10273
|
worktreePrompt(plan),
|
|
9599
10274
|
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
9600
|
-
todosStartLine(todosProjectPath, input.taskId),
|
|
9601
|
-
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence and blockers")
|
|
9602
|
-
]),
|
|
10275
|
+
todosStartLine(todosProjectPath, input.taskId, todosDbPath),
|
|
10276
|
+
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence and blockers", todosDbPath)
|
|
10277
|
+
], todosDbPath),
|
|
9603
10278
|
"Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
|
|
9604
10279
|
"Inspect the repository/project state, implement only the task scope, run focused validation, preserve unrelated user changes, and update the task with comments, evidence, changed files, commits, and blockers.",
|
|
9605
10280
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
@@ -9612,9 +10287,9 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9612
10287
|
...goalHeaderFragment(`Verify todos task ${input.taskId} after the worker step.`, "verifier", "task"),
|
|
9613
10288
|
worktreePrompt(plan),
|
|
9614
10289
|
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
9615
|
-
todosVerificationLine(todosProjectPath, input.taskId),
|
|
9616
|
-
todosDoneLine(todosProjectPath, input.taskId)
|
|
9617
|
-
]),
|
|
10290
|
+
todosVerificationLine(todosProjectPath, input.taskId, todosDbPath),
|
|
10291
|
+
todosDoneLine(todosProjectPath, input.taskId, todosDbPath)
|
|
10292
|
+
], todosDbPath),
|
|
9618
10293
|
adversarialReviewFragment("the task, repository state, commits, tests, and worker evidence", TASK_REVIEW_FOCUS),
|
|
9619
10294
|
verifierRuntimeGuidance(input),
|
|
9620
10295
|
TASK_VERIFIER_DECISION_FRAGMENT,
|
|
@@ -9629,7 +10304,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9629
10304
|
description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
|
|
9630
10305
|
version: 1,
|
|
9631
10306
|
steps: [
|
|
9632
|
-
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before worker execution when the source todos task is not resolvable."),
|
|
10307
|
+
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before worker execution when the source todos task is not resolvable.", todosDbPath),
|
|
9633
10308
|
...workerVerifierSteps({
|
|
9634
10309
|
input,
|
|
9635
10310
|
seed: input.taskId,
|
|
@@ -9649,6 +10324,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9649
10324
|
if (!input.projectPath?.trim())
|
|
9650
10325
|
throw new Error("projectPath is required");
|
|
9651
10326
|
const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
|
|
10327
|
+
const todosDbPath = input.todosDbPath;
|
|
9652
10328
|
const plan = worktreePlan(input, input.taskId);
|
|
9653
10329
|
const taskContext = {
|
|
9654
10330
|
taskId: input.taskId,
|
|
@@ -9660,6 +10336,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9660
10336
|
routeProjectPath: input.routeProjectPath,
|
|
9661
10337
|
projectGroup: input.projectGroup,
|
|
9662
10338
|
todosProjectPath,
|
|
10339
|
+
todosDbPath,
|
|
9663
10340
|
worktree: worktreeContextFragment(plan)
|
|
9664
10341
|
};
|
|
9665
10342
|
const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
@@ -9673,8 +10350,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9673
10350
|
const shared = [
|
|
9674
10351
|
worktreePrompt(plan),
|
|
9675
10352
|
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
9676
|
-
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker")
|
|
9677
|
-
]),
|
|
10353
|
+
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker", todosDbPath)
|
|
10354
|
+
], todosDbPath),
|
|
9678
10355
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
9679
10356
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
9680
10357
|
"",
|
|
@@ -9683,7 +10360,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9683
10360
|
].join(`
|
|
9684
10361
|
`);
|
|
9685
10362
|
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
9686
|
-
const blockTaskCommand =
|
|
10363
|
+
const blockTaskCommand = `${todosEnvPrefix(todosDbPath)}todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
9687
10364
|
const gateStopFragment = (stage, stops) => `The deterministic ${stage} gate will stop ${stops} unless the latest ${stage} marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state.`;
|
|
9688
10365
|
const triagePrompt = [
|
|
9689
10366
|
...goalHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
@@ -9711,7 +10388,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9711
10388
|
const workerPrompt = [
|
|
9712
10389
|
...goalHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
9713
10390
|
shared,
|
|
9714
|
-
todosStartLine(todosProjectPath, input.taskId),
|
|
10391
|
+
todosStartLine(todosProjectPath, input.taskId, todosDbPath),
|
|
9715
10392
|
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits, evidence, blockers, and residual risks.",
|
|
9716
10393
|
input.prHandoff ? `When only GitHub network access is blocked after a successful commit/validation, record the handoff artifact at ${handoffArtifactPath} instead of repeatedly retrying push/PR creation.` : undefined,
|
|
9717
10394
|
WORKER_LEAVES_COMPLETION_FRAGMENT
|
|
@@ -9720,8 +10397,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9720
10397
|
const verifierPrompt = [
|
|
9721
10398
|
...goalHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
9722
10399
|
shared,
|
|
9723
|
-
todosVerificationLine(todosProjectPath, input.taskId),
|
|
9724
|
-
todosDoneLine(todosProjectPath, input.taskId),
|
|
10400
|
+
todosVerificationLine(todosProjectPath, input.taskId, todosDbPath),
|
|
10401
|
+
todosDoneLine(todosProjectPath, input.taskId, todosDbPath),
|
|
9725
10402
|
adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
|
|
9726
10403
|
verifierRuntimeGuidance(input),
|
|
9727
10404
|
input.prHandoff ? `If ${handoffArtifactPath} exists and there is no PR URL evidence, verify that the PR handoff step queued or completed a bounded handoff; leave the original task open or blocked until PR evidence is recorded.` : undefined,
|
|
@@ -9730,7 +10407,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9730
10407
|
].filter(Boolean).join(`
|
|
9731
10408
|
`);
|
|
9732
10409
|
const steps = [
|
|
9733
|
-
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before lifecycle agents execute when the source todos task is not resolvable."),
|
|
10410
|
+
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before lifecycle agents execute when the source todos task is not resolvable.", todosDbPath),
|
|
9734
10411
|
{
|
|
9735
10412
|
id: "triage",
|
|
9736
10413
|
name: "Triage",
|
|
@@ -9744,6 +10421,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9744
10421
|
description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
|
|
9745
10422
|
dependsOn: ["triage"],
|
|
9746
10423
|
todosProjectPath,
|
|
10424
|
+
todosDbPath,
|
|
9747
10425
|
taskId: input.taskId,
|
|
9748
10426
|
goMarker: gateMarker("triage", "go"),
|
|
9749
10427
|
blockedMarker: gateMarker("triage", "blocked"),
|
|
@@ -9762,6 +10440,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9762
10440
|
description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
|
|
9763
10441
|
dependsOn: ["planner"],
|
|
9764
10442
|
todosProjectPath,
|
|
10443
|
+
todosDbPath,
|
|
9765
10444
|
taskId: input.taskId,
|
|
9766
10445
|
goMarker: gateMarker("planner", "go"),
|
|
9767
10446
|
blockedMarker: gateMarker("planner", "blocked"),
|
|
@@ -9777,7 +10456,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9777
10456
|
}
|
|
9778
10457
|
];
|
|
9779
10458
|
if (input.prHandoff) {
|
|
9780
|
-
steps.push(prHandoffStep(input, plan, todosProjectPath));
|
|
10459
|
+
steps.push(prHandoffStep(input, plan, todosProjectPath, todosDbPath));
|
|
9781
10460
|
}
|
|
9782
10461
|
steps.push({
|
|
9783
10462
|
id: "verifier",
|
|
@@ -10002,6 +10681,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
10002
10681
|
taskTitle: values.taskTitle,
|
|
10003
10682
|
taskDescription: values.taskDescription,
|
|
10004
10683
|
todosProjectPath: values.todosProjectPath ?? values.todosProject,
|
|
10684
|
+
todosDbPath: values.todosDbPath,
|
|
10005
10685
|
triageAuthProfile: values.triageAuthProfile,
|
|
10006
10686
|
plannerAuthProfile: values.plannerAuthProfile,
|
|
10007
10687
|
triageAccount: accountVar(values.triageAccount, values.accountTool),
|
|
@@ -10066,6 +10746,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
10066
10746
|
taskTitle: values.taskTitle,
|
|
10067
10747
|
taskDescription: values.taskDescription,
|
|
10068
10748
|
todosProjectPath: values.todosProjectPath ?? values.todosProject,
|
|
10749
|
+
todosDbPath: values.todosDbPath,
|
|
10069
10750
|
eventId: values.eventId,
|
|
10070
10751
|
eventType: values.eventType
|
|
10071
10752
|
});
|
|
@@ -10395,9 +11076,12 @@ export {
|
|
|
10395
11076
|
executeLoopTarget,
|
|
10396
11077
|
executeLoop,
|
|
10397
11078
|
deploymentStatusLine,
|
|
11079
|
+
createSqliteLoopStorage,
|
|
11080
|
+
createPostgresStorage,
|
|
10398
11081
|
createLoopsMcpServer,
|
|
10399
11082
|
computeNextAfter,
|
|
10400
11083
|
classifyRunFailure,
|
|
11084
|
+
checksumStorageSql,
|
|
10401
11085
|
buildScriptInventoryReport,
|
|
10402
11086
|
buildNameHygieneReport,
|
|
10403
11087
|
buildHealthReport,
|
|
@@ -10406,6 +11090,10 @@ export {
|
|
|
10406
11090
|
ValidationError,
|
|
10407
11091
|
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
10408
11092
|
Store,
|
|
11093
|
+
SqliteLoopStorage,
|
|
11094
|
+
PostgresStorage,
|
|
11095
|
+
POSTGRES_STORAGE_MIGRATIONS,
|
|
11096
|
+
POSTGRES_MIGRATION_LEDGER_TABLE,
|
|
10409
11097
|
LoopsClient,
|
|
10410
11098
|
LoopNotFoundError,
|
|
10411
11099
|
LoopArchivedError,
|