@hasna/loops 0.4.12 → 0.4.13

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/dist/sdk/index.js CHANGED
@@ -4266,7 +4266,7 @@ class Store {
4266
4266
  // package.json
4267
4267
  var package_default = {
4268
4268
  name: "@hasna/loops",
4269
- version: "0.4.12",
4269
+ version: "0.4.13",
4270
4270
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4271
4271
  type: "module",
4272
4272
  main: "dist/index.js",
@@ -4457,6 +4457,50 @@ function sourceOfTruthForMode(mode) {
4457
4457
  return "self_hosted_control_plane";
4458
4458
  return "cloud_control_plane";
4459
4459
  }
4460
+ var ROUTE_ADMISSION_GATES = [
4461
+ "max_dispatch",
4462
+ "max_active",
4463
+ "max_active_per_project",
4464
+ "max_active_per_project_group",
4465
+ "max_active_scope",
4466
+ "max_per_profile"
4467
+ ];
4468
+ function remoteSchedulerBackendForMode(mode, config) {
4469
+ if (mode === "local")
4470
+ return "none";
4471
+ if (mode === "cloud")
4472
+ return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
4473
+ if (config.databaseUrlPresent)
4474
+ return "postgres_contract";
4475
+ if (config.apiUrl)
4476
+ return "api_control_plane_contract";
4477
+ return "unconfigured";
4478
+ }
4479
+ function schedulerStateForMode(args) {
4480
+ const nonLocal = args.deploymentMode !== "local";
4481
+ return {
4482
+ authority: args.sourceOfTruth,
4483
+ localStore: {
4484
+ backend: "sqlite",
4485
+ role: args.localRole,
4486
+ runArtifacts: "local_files",
4487
+ routeAdmissionState: "workflow_work_items"
4488
+ },
4489
+ remoteStore: {
4490
+ backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
4491
+ configured: nonLocal && args.controlPlaneConfigured,
4492
+ applySupported: false,
4493
+ objectArtifacts: nonLocal ? "object_store_contract" : "none",
4494
+ mutatesAws: false
4495
+ },
4496
+ routeAdmission: {
4497
+ stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
4498
+ activeStatuses: ["admitted", "running"],
4499
+ gates: ROUTE_ADMISSION_GATES,
4500
+ dryRunEvaluatesLiveCounts: false
4501
+ }
4502
+ };
4503
+ }
4460
4504
  function displayControlPlaneUrl(value) {
4461
4505
  if (!value)
4462
4506
  return;
@@ -4482,6 +4526,9 @@ function buildDeploymentStatus(opts = {}) {
4482
4526
  if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
4483
4527
  warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
4484
4528
  }
4529
+ if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
4530
+ warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
4531
+ }
4485
4532
  if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
4486
4533
  warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
4487
4534
  }
@@ -4515,6 +4562,13 @@ function buildDeploymentStatus(opts = {}) {
4515
4562
  required: deploymentMode !== "local",
4516
4563
  role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
4517
4564
  },
4565
+ schedulerState: schedulerStateForMode({
4566
+ deploymentMode,
4567
+ sourceOfTruth: sourceOfTruthForMode(deploymentMode),
4568
+ localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
4569
+ controlPlaneConfigured,
4570
+ config
4571
+ }),
4518
4572
  warnings
4519
4573
  };
4520
4574
  }
@@ -4527,6 +4581,7 @@ function deploymentStatusLine(status) {
4527
4581
  `source=${status.deploymentModeSource}`,
4528
4582
  `truth=${status.sourceOfTruth}`,
4529
4583
  `local=${status.localStore.role}`,
4584
+ `scheduler=${status.schedulerState.routeAdmission.stateStore}`,
4530
4585
  `control_plane=${configured}`
4531
4586
  ].join(" ");
4532
4587
  }
@@ -5827,6 +5882,21 @@ function runDoctor(store) {
5827
5882
  checks.push(status.running ? { id: "daemon", status: "ok", message: `daemon is running pid=${status.pid}` } : { id: "daemon", status: status.stale ? "warn" : "ok", message: status.stale ? "daemon pid file is stale" : "daemon is not running" });
5828
5883
  const failedRuns = store.countRuns("failed");
5829
5884
  checks.push(failedRuns === 0 ? { id: "loop-runs", status: "ok", message: "no failed loop runs recorded" } : { id: "loop-runs", status: "warn", message: `${failedRuns} failed loop run(s) recorded` });
5885
+ const deployment = buildDeploymentStatus();
5886
+ const schedulerState = deployment.schedulerState;
5887
+ checks.push({
5888
+ id: "scheduler-state",
5889
+ status: deployment.deploymentMode === "local" || deployment.controlPlane.configured ? "ok" : "warn",
5890
+ message: `scheduler state authority=${schedulerState.authority} local=${schedulerState.localStore.role} remote=${schedulerState.remoteStore.backend}`,
5891
+ detail: [
5892
+ `route_state=${schedulerState.routeAdmission.stateStore}`,
5893
+ `active_statuses=${schedulerState.routeAdmission.activeStatuses.join(",")}`,
5894
+ `gates=${schedulerState.routeAdmission.gates.join(",")}`,
5895
+ `artifacts=${schedulerState.localStore.runArtifacts}`,
5896
+ `remote_artifacts=${schedulerState.remoteStore.objectArtifacts}`,
5897
+ `remote_apply=${String(schedulerState.remoteStore.applySupported)}`
5898
+ ].join(" ")
5899
+ });
5830
5900
  for (const loop of store.listLoops({ status: "active" })) {
5831
5901
  try {
5832
5902
  if (loop.target.type === "workflow") {
@@ -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
@@ -674,11 +684,12 @@ idempotency key before rendering worktree plans or checking route limits. In
674
684
  dry-run mode, throttle counts are not evaluated because opening the live loop
675
685
  store can create or migrate the local database.
676
686
  Terminal routed work items such as failed, dead-letter, cancelled, or succeeded
677
- history remain deduped until an operator runs `loops routes requeue
678
- <work-item-id> --reason "<cause fixed>"`; a later drain should not create
679
- another worker just because the previous workflow ended. The next route-created
680
- output records `requeue` evidence with the previous work item id, previous
681
- attempts, operator reason, new attempt, workflow id, and loop id.
687
+ history are re-admitted only when the todos task is still actionable, the
688
+ per-attempt backoff has elapsed, and the redispatch cap has not been reached.
689
+ Operators can still force a retry with `loops routes requeue <work-item-id>
690
+ --reason "<cause fixed>"`. The next route-created output records `requeue`
691
+ evidence with the previous work item id, previous attempts, reason, new attempt,
692
+ workflow id, and loop id.
682
693
 
683
694
  When a sandboxed Codewith/Codex worker must update app stores outside the repo
684
695
  worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
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",