@hasna/loops 0.4.12 → 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 +106 -3
- package/README.md +70 -17
- package/dist/api/index.d.ts +35 -0
- package/dist/api/index.js +750 -10
- package/dist/cli/index.js +1744 -361
- 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 +3 -3
- package/dist/index.js +6513 -4840
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/mode.d.ts +27 -0
- package/dist/lib/mode.js +69 -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 +1447 -479
- package/dist/runner/index.js +179 -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 +1106 -296
- 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/DEPLOYMENT_MODES.md +23 -1
- package/docs/USAGE.md +66 -16
- 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/DEPLOYMENT_MODES.md
CHANGED
|
@@ -68,7 +68,7 @@ loops import ./loops-export.json --apply
|
|
|
68
68
|
Human status output is intentionally compact:
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
|
-
deploymentMode=local active source=default truth=local_sqlite local=authoritative control_plane=none
|
|
71
|
+
deploymentMode=local active source=default truth=local_sqlite local=authoritative scheduler=local_sqlite control_plane=none
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
JSON uses these field names:
|
|
@@ -85,6 +85,22 @@ JSON uses these field names:
|
|
|
85
85
|
presence signal.
|
|
86
86
|
- `controlPlane.apiUrl`: a display-safe URL without credentials, query string,
|
|
87
87
|
or fragment.
|
|
88
|
+
- `schedulerState.localStore`: always names the local SQLite store and local
|
|
89
|
+
run artifact files. In `local` mode this store is authoritative; in
|
|
90
|
+
non-local modes it is a cache, offline spool, and audit copy.
|
|
91
|
+
- `schedulerState.remoteStore`: names the remote scheduler contract:
|
|
92
|
+
`api_control_plane_contract`, `postgres_contract`,
|
|
93
|
+
`hosted_control_plane_contract`, `unconfigured`, or `none`. Remote apply is
|
|
94
|
+
`false` in this public package until a control-plane host wires a full
|
|
95
|
+
storage adapter.
|
|
96
|
+
- `schedulerState.remoteStore.objectArtifacts`: `object_store_contract` means
|
|
97
|
+
remote artifact/object storage is a control-plane contract. The public package
|
|
98
|
+
does not create or mutate S3 buckets, AWS resources, or hosted credentials.
|
|
99
|
+
- `schedulerState.routeAdmission`: names the active route-state store and the
|
|
100
|
+
bounded gates (`max_dispatch`, `max_active`, `max_active_per_project`,
|
|
101
|
+
`max_active_per_project_group`, `max_active_scope`, and `max_per_profile`).
|
|
102
|
+
Live active counts use admitted/running work items; dry-runs do not open or
|
|
103
|
+
migrate the live store to compute counts.
|
|
88
104
|
|
|
89
105
|
`loops-api` is a separate process in the same public package. It is not a
|
|
90
106
|
separate package at this stage because self-hosted users and the hosted service
|
|
@@ -150,6 +166,12 @@ would generate new ids, so it is not a no-loss migration. Local SQLite remains
|
|
|
150
166
|
authoritative until a safe import is applied; in non-local modes it may remain
|
|
151
167
|
a cache, offline spool, and audit copy.
|
|
152
168
|
|
|
169
|
+
`LOOPS_DATABASE_URL` selects the self-hosted Postgres scheduler-state contract,
|
|
170
|
+
but it does not make the standalone CLI mutate a remote database by itself.
|
|
171
|
+
Remote execution still flows through a configured control-plane API and runner
|
|
172
|
+
protocol. `loops-runner` needs `LOOPS_API_URL` or `HASNA_LOOPS_API_URL` to claim
|
|
173
|
+
work; a database URL alone is migration/readiness configuration.
|
|
174
|
+
|
|
153
175
|
`loops self-hosted runner-register` is also preview-only unless `--apply` is
|
|
154
176
|
present. The dry run prints the runner id, machine id, labels, and
|
|
155
177
|
capabilities that would be posted, without exposing tokens.
|
package/docs/USAGE.md
CHANGED
|
@@ -25,6 +25,16 @@ planes:
|
|
|
25
25
|
status only, and requires `LOOPS_CLOUD_API_URL` plus `LOOPS_CLOUD_TOKEN` or
|
|
26
26
|
`HASNA_LOOPS_CLOUD_TOKEN` before status can report ready.
|
|
27
27
|
|
|
28
|
+
Scheduler state is explicit in status JSON. `schedulerState.localStore` is
|
|
29
|
+
SQLite plus local run artifact files: authoritative in `local`, cache/spool in
|
|
30
|
+
non-local modes. `schedulerState.remoteStore` names the non-local contract
|
|
31
|
+
(`api_control_plane_contract`, `postgres_contract`, or
|
|
32
|
+
`hosted_control_plane_contract`) and reports `applySupported=false` because this
|
|
33
|
+
public package does not directly mutate remote Postgres, S3/object storage, AWS
|
|
34
|
+
resources, or hosted credentials. Route admission remains bounded by
|
|
35
|
+
`max_dispatch`, `max_active`, `max_active_per_project`,
|
|
36
|
+
`max_active_per_project_group`, `max_active_scope`, and `max_per_profile`.
|
|
37
|
+
|
|
28
38
|
Useful status commands:
|
|
29
39
|
|
|
30
40
|
```bash
|
|
@@ -468,21 +478,39 @@ worker finishes; pass `--verifier-idle-timeout none` or template variable
|
|
|
468
478
|
`verifierIdleTimeoutMs=none` only when another heartbeat is guaranteed. Use a
|
|
469
479
|
positive numeric `timeoutMs` only when an agentic step is intentionally bounded.
|
|
470
480
|
|
|
471
|
-
To migrate existing
|
|
472
|
-
|
|
473
|
-
|
|
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:
|
|
474
486
|
|
|
475
487
|
```bash
|
|
476
488
|
loops workflows migrate-agent-timeouts --loop <loop-id-or-name>
|
|
477
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
|
|
478
493
|
```
|
|
479
494
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
486
514
|
re-delivering or draining the original task/event route.
|
|
487
515
|
|
|
488
516
|
```json
|
|
@@ -674,11 +702,12 @@ idempotency key before rendering worktree plans or checking route limits. In
|
|
|
674
702
|
dry-run mode, throttle counts are not evaluated because opening the live loop
|
|
675
703
|
store can create or migrate the local database.
|
|
676
704
|
Terminal routed work items such as failed, dead-letter, cancelled, or succeeded
|
|
677
|
-
history
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
705
|
+
history are re-admitted only when the todos task is still actionable, the
|
|
706
|
+
per-attempt backoff has elapsed, and the redispatch cap has not been reached.
|
|
707
|
+
Operators can still force a retry with `loops routes requeue <work-item-id>
|
|
708
|
+
--reason "<cause fixed>"`. The next route-created output records `requeue`
|
|
709
|
+
evidence with the previous work item id, previous attempts, reason, new attempt,
|
|
710
|
+
workflow id, and loop id.
|
|
682
711
|
|
|
683
712
|
When a sandboxed Codewith/Codex worker must update app stores outside the repo
|
|
684
713
|
worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
|
|
@@ -854,25 +883,46 @@ agent-run failures for default-loop SLOs:
|
|
|
854
883
|
|
|
855
884
|
```bash
|
|
856
885
|
loops health --json
|
|
886
|
+
loops health scan --include active,paused --latest-run --doctor --daemon --json
|
|
857
887
|
loops expectations <loop-id-or-name> --json
|
|
858
888
|
```
|
|
859
889
|
|
|
860
890
|
The JSON contains the expectation result, bounded error/stdout/stderr evidence,
|
|
861
891
|
a stable failure fingerprint, route metadata, and recommended task fields.
|
|
862
|
-
OpenLoops does not mutate Todos from `health
|
|
863
|
-
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:
|
|
864
895
|
|
|
865
896
|
```bash
|
|
866
897
|
loops health route-tasks \
|
|
867
898
|
--project ~/.hasna/loops \
|
|
868
899
|
--task-list loop-error-self-heal \
|
|
869
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
|
|
870
911
|
```
|
|
871
912
|
|
|
872
913
|
Use `--dry-run --json` first when testing a new automation path. Routed tasks
|
|
873
914
|
include the stable failure fingerprint, classification, loop id/name, and
|
|
874
915
|
`no_tmux_dispatch=true` metadata.
|
|
875
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
|
+
|
|
876
926
|
Use `--evidence-dir <dir>` when a deterministic loop needs a compact JSON
|
|
877
927
|
heartbeat/report on disk. Use `--auto-route` only on task lists that should feed
|
|
878
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": {
|