@hasna/loops 0.4.13 → 0.4.14
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 +74 -3
- package/README.md +54 -12
- package/dist/api/index.d.ts +35 -0
- package/dist/api/index.js +695 -10
- package/dist/cli/index.js +1633 -366
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +948 -212
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3567 -2010
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/mode.js +14 -2
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +1329 -211
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +114 -0
- package/dist/lib/storage/sqlite.js +336 -8
- package/dist/lib/store.d.ts +234 -0
- package/dist/lib/store.js +350 -8
- package/dist/mcp/index.js +1067 -306
- package/dist/runner/index.js +124 -25
- package/dist/sdk/http.d.ts +115 -0
- package/dist/sdk/http.js +145 -0
- package/dist/sdk/index.d.ts +5 -1
- package/dist/sdk/index.js +1052 -312
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +7619 -0
- package/dist/types.d.ts +3 -0
- package/docs/CUTOVER-RUNBOOK.md +61 -0
- package/docs/USAGE.md +50 -11
- package/package.json +14 -2
package/dist/types.d.ts
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Loops Postgres Cutover Runbook
|
|
2
|
+
|
|
3
|
+
Status: **backend foundation landed; daemon still runs on local sqlite.** Do NOT
|
|
4
|
+
flip the running daemon to Postgres until every step below is green.
|
|
5
|
+
|
|
6
|
+
PURE REMOTE (Amendment A1): in cloud mode, reads AND writes hit cloud Postgres
|
|
7
|
+
directly. There is no hybrid/sync/cache mode. After a verified migration the
|
|
8
|
+
local sqlite file is renamed to `<name>.db.pre-cloud-2026-07-06.bak`.
|
|
9
|
+
|
|
10
|
+
## What shipped in this PR
|
|
11
|
+
|
|
12
|
+
- Vendored `@hasna/contracts` storage kit → `src/generated/storage-kit/`
|
|
13
|
+
(pool/query/tls/mode/migrations/health). Regenerate: `bunx @hasna/contracts vendor-kit`.
|
|
14
|
+
- `src/lib/storage/pg-executor.ts` — `PgPoolExecutor` adapting the vendored
|
|
15
|
+
`pg.Pool` client to `PostgresQueryExecutor` (drives `PostgresStorage.migrate`).
|
|
16
|
+
- `src/lib/storage/pg-runner-claim.ts` — `claimNextRun` / `heartbeatRunLease`
|
|
17
|
+
using `SELECT ... FOR UPDATE SKIP LOCKED` for concurrent runners.
|
|
18
|
+
- `src/lib/storage/postgres-concurrency.test.ts` — two-connection race test
|
|
19
|
+
(skips unless `LOOPS_TEST_DATABASE_URL` is set; verified green against a
|
|
20
|
+
throwaway local Postgres 16).
|
|
21
|
+
- `pg` promoted to a direct dependency; `@types/pg` added as devDependency.
|
|
22
|
+
|
|
23
|
+
## Remaining before daemon cutover (tracked, NOT done here)
|
|
24
|
+
|
|
25
|
+
1. **Full `PostgresLoopStorage` implementing `LoopStorageContract`** (60+ methods
|
|
26
|
+
in `src/lib/storage/contract.ts`). Only the migration ledger + claim/lease
|
|
27
|
+
primitives exist today. The claim primitive here is the correctness core to
|
|
28
|
+
build the rest on.
|
|
29
|
+
2. **Async consumer wiring.** All call sites construct `new Store()` synchronously
|
|
30
|
+
(`src/cli`, `src/daemon`, `src/sdk`, `src/mcp`, `src/lib/route`). PURE REMOTE
|
|
31
|
+
requires routing them through the async `LoopStorageContract` when
|
|
32
|
+
`LOOPS_DATABASE_URL` is set (mode resolved by `src/lib/mode.ts`, which stays
|
|
33
|
+
authoritative). `SqliteLoopStorage` already wraps the sync store for the
|
|
34
|
+
local path.
|
|
35
|
+
3. **`loops migrate-local` command** (data migration TOOLING only): copy
|
|
36
|
+
definitions + schedules + last 30 days of runs from local sqlite into cloud
|
|
37
|
+
Postgres, then rename the sqlite file to the `.pre-cloud-2026-07-06.bak`
|
|
38
|
+
backup. Does NOT enable cloud mode.
|
|
39
|
+
4. **Apply schema to shared RDS** via SSM tunnel on local port 15438 using the
|
|
40
|
+
`hasna/oss/loops/database-url-owner` secret, then run
|
|
41
|
+
`PostgresStorage(new PgPoolExecutor(...)).migrate()` (dry-run first). Kill the
|
|
42
|
+
tunnel and remove temp files afterward. Never run the test suite against RDS.
|
|
43
|
+
|
|
44
|
+
## Enable steps (run only after 1–4 are green)
|
|
45
|
+
|
|
46
|
+
1. Confirm `PostgresStorage.migrate({ dryRun: true })` reports all four
|
|
47
|
+
migrations already applied on the loops RDS database.
|
|
48
|
+
2. Stop the loops daemon: `loops daemon stop`.
|
|
49
|
+
3. `loops migrate-local --database-url "$LOOPS_DATABASE_URL"` (moves defs +
|
|
50
|
+
schedules + 30d of runs; verify counts; sqlite renamed to backup).
|
|
51
|
+
4. Export `LOOPS_DATABASE_URL` (from `hasna/oss/loops/database-url`) and the
|
|
52
|
+
cloud storage-mode env for the daemon unit; restart the daemon.
|
|
53
|
+
5. Verify `loops status` reports `deploymentMode: cloud`, `sourceOfTruth:
|
|
54
|
+
cloud_control_plane`, and that a test loop run claims + finalizes against
|
|
55
|
+
Postgres.
|
|
56
|
+
|
|
57
|
+
## Rollback
|
|
58
|
+
|
|
59
|
+
Cloud mode is off until `LOOPS_DATABASE_URL` is present in the daemon env.
|
|
60
|
+
To roll back: unset it, restore the sqlite backup
|
|
61
|
+
(`mv <name>.db.pre-cloud-2026-07-06.bak <name>.db`), restart the daemon.
|
package/docs/USAGE.md
CHANGED
|
@@ -478,21 +478,39 @@ worker finishes; pass `--verifier-idle-timeout none` or template variable
|
|
|
478
478
|
`verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
|
|
479
479
|
positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
|
|
480
480
|
|
|
481
|
-
To migrate existing
|
|
482
|
-
|
|
483
|
-
|
|
481
|
+
To migrate existing agentic loops, use the timeout migrator instead of editing
|
|
482
|
+
the database directly. Workflow loops are migrated append-only because
|
|
483
|
+
historical workflow runs must keep pointing at their original spec; direct
|
|
484
|
+
agent loops selected with `--loop` update their stored target in place for
|
|
485
|
+
future executions:
|
|
484
486
|
|
|
485
487
|
```bash
|
|
486
488
|
loops workflows migrate-agent-timeouts --loop <loop-id-or-name>
|
|
487
489
|
loops workflows migrate-agent-timeouts --loop <loop-id-or-name> --apply
|
|
490
|
+
loops workflows migrate-goal-wrappers --loop <loop-id-or-name>
|
|
491
|
+
loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply
|
|
492
|
+
loops workflows migrate-goal-wrappers --loop <loop-id-or-name> --apply --archive-old
|
|
488
493
|
```
|
|
489
494
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
495
|
+
Both migrators dry-run by default. For eligible non-running workflow loops,
|
|
496
|
+
`--apply` creates a new workflow spec and retargets only future executions;
|
|
497
|
+
historical workflow runs keep pointing at their original spec. For direct agent
|
|
498
|
+
loops selected with `--loop`, `migrate-agent-timeouts --apply` updates the
|
|
499
|
+
stored target in place for future executions. Use `--archive-old` to archive
|
|
500
|
+
superseded workflow specs when no active loops still reference them.
|
|
501
|
+
|
|
502
|
+
`migrate-agent-timeouts` clones the workflow with the requested agent timeout
|
|
503
|
+
policy (`--timeout none` by default). `migrate-goal-wrappers` targets loops that
|
|
504
|
+
define both a loop-level goal and a redundant workflow-level top-level goal: it
|
|
505
|
+
clones a goal-free workflow spec, retargets the loop, and leaves the loop-level
|
|
506
|
+
goal as the sole orchestration wrapper. Loops with only a workflow-level goal or
|
|
507
|
+
only a loop-level goal are skipped. New workflow loops cannot combine both
|
|
508
|
+
wrappers; use loop-level `--goal` on `loops create workflow` instead of nesting
|
|
509
|
+
a top-level `"goal"` in the workflow JSON.
|
|
510
|
+
|
|
511
|
+
Use `loops workflows recover` only for interrupted `running` workflow runs whose
|
|
512
|
+
recorded child process is gone; terminal `timed_out` runs must be requeued with
|
|
513
|
+
`loops routes requeue <work-item-id> --reason "<cause fixed>"` before
|
|
496
514
|
re-delivering or draining the original task/event route.
|
|
497
515
|
|
|
498
516
|
```json
|
|
@@ -865,25 +883,46 @@ agent-run failures for default-loop SLOs:
|
|
|
865
883
|
|
|
866
884
|
```bash
|
|
867
885
|
loops health --json
|
|
886
|
+
loops health scan --include active,paused --latest-run --doctor --daemon --json
|
|
868
887
|
loops expectations <loop-id-or-name> --json
|
|
869
888
|
```
|
|
870
889
|
|
|
871
890
|
The JSON contains the expectation result, bounded error/stdout/stderr evidence,
|
|
872
891
|
a stable failure fingerprint, route metadata, and recommended task fields.
|
|
873
|
-
OpenLoops does not mutate Todos from `health
|
|
874
|
-
expectations into deduped tasks,
|
|
892
|
+
OpenLoops does not mutate Todos from `health`, `expectations`, or read-only
|
|
893
|
+
`health scan`. To turn failed expectations or scan findings into deduped tasks,
|
|
894
|
+
use an explicit mutating command:
|
|
875
895
|
|
|
876
896
|
```bash
|
|
877
897
|
loops health route-tasks \
|
|
878
898
|
--project ~/.hasna/loops \
|
|
879
899
|
--task-list loop-error-self-heal \
|
|
880
900
|
--max-actions 5
|
|
901
|
+
|
|
902
|
+
loops health scan \
|
|
903
|
+
--include active,paused \
|
|
904
|
+
--latest-run \
|
|
905
|
+
--doctor \
|
|
906
|
+
--daemon \
|
|
907
|
+
--upsert-todos \
|
|
908
|
+
--dry-run \
|
|
909
|
+
--max-actions 5 \
|
|
910
|
+
--evidence-dir ~/.hasna/loops/reports/health-scan
|
|
881
911
|
```
|
|
882
912
|
|
|
883
913
|
Use `--dry-run --json` first when testing a new automation path. Routed tasks
|
|
884
914
|
include the stable failure fingerprint, classification, loop id/name, and
|
|
885
915
|
`no_tmux_dispatch=true` metadata.
|
|
886
916
|
|
|
917
|
+
`health scan` replaces local loop-error self-heal scripts with package-owned
|
|
918
|
+
CLI/SDK/MCP primitives. It inventories included loop statuses, detects daemon,
|
|
919
|
+
doctor, preflight, latest-run, and stale-running issues, writes bounded
|
|
920
|
+
`summary.json` and `report.md` files under
|
|
921
|
+
`$LOOPS_DATA_DIR/reports/health-scan` or `--report-dir`/`--evidence-dir`, and
|
|
922
|
+
keeps output compact. It is read-only by default. The only safe self-heal is
|
|
923
|
+
`--start-daemon`, which starts the daemon only when status proves it is not
|
|
924
|
+
running; it does not stop, resume, archive, delete, or reap loops.
|
|
925
|
+
|
|
887
926
|
Use `--evidence-dir <dir>` when a deterministic loop needs a compact JSON
|
|
888
927
|
heartbeat/report on disk. Use `--auto-route` only on task lists that should feed
|
|
889
928
|
the task-created headless worker/verifier workflow; it adds the `auto:route`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/loops",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"loops": "dist/cli/index.js",
|
|
10
10
|
"loops-daemon": "dist/daemon/index.js",
|
|
11
11
|
"loops-api": "dist/api/index.js",
|
|
12
|
+
"loops-serve": "dist/serve/index.js",
|
|
12
13
|
"loops-runner": "dist/runner/index.js",
|
|
13
14
|
"loops-mcp": "dist/mcp/index.js"
|
|
14
15
|
},
|
|
@@ -21,6 +22,14 @@
|
|
|
21
22
|
"types": "./dist/sdk/index.d.ts",
|
|
22
23
|
"import": "./dist/sdk/index.js"
|
|
23
24
|
},
|
|
25
|
+
"./sdk/http": {
|
|
26
|
+
"types": "./dist/sdk/http.d.ts",
|
|
27
|
+
"import": "./dist/sdk/http.js"
|
|
28
|
+
},
|
|
29
|
+
"./serve": {
|
|
30
|
+
"types": "./dist/serve/index.d.ts",
|
|
31
|
+
"import": "./dist/serve/index.js"
|
|
32
|
+
},
|
|
24
33
|
"./mcp": {
|
|
25
34
|
"types": "./dist/mcp/index.d.ts",
|
|
26
35
|
"import": "./dist/mcp/index.js"
|
|
@@ -66,7 +75,7 @@
|
|
|
66
75
|
"LICENSE"
|
|
67
76
|
],
|
|
68
77
|
"scripts": {
|
|
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",
|
|
78
|
+
"build": "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.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/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
70
79
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
71
80
|
"typecheck": "tsc --noEmit",
|
|
72
81
|
"test": "bun test",
|
|
@@ -101,11 +110,13 @@
|
|
|
101
110
|
"bun": ">=1.0.0"
|
|
102
111
|
},
|
|
103
112
|
"dependencies": {
|
|
113
|
+
"@hasna/contracts": "^0.4.1",
|
|
104
114
|
"@hasna/events": "^0.1.9",
|
|
105
115
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
106
116
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
107
117
|
"ai": "6.0.204",
|
|
108
118
|
"commander": "^13.1.0",
|
|
119
|
+
"pg": "^8.13.1",
|
|
109
120
|
"zod": "4.4.3"
|
|
110
121
|
},
|
|
111
122
|
"optionalDependencies": {
|
|
@@ -113,6 +124,7 @@
|
|
|
113
124
|
},
|
|
114
125
|
"devDependencies": {
|
|
115
126
|
"@types/bun": "latest",
|
|
127
|
+
"@types/pg": "^8.11.10",
|
|
116
128
|
"typescript": "^5.7.3"
|
|
117
129
|
},
|
|
118
130
|
"publishConfig": {
|