loopctl-mcp-server 2.99.0 → 2.100.0
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 +3 -3
- package/index.js +37 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -504,11 +504,11 @@ Enroll the dev machines that run the agent delivery loop, see which are connecte
|
|
|
504
504
|
|
|
505
505
|
| Tool | Description |
|
|
506
506
|
|---|---|
|
|
507
|
-
| `runner_enroll` | Enroll this machine as a runner (`POST /api/v1/runners`). Required: `name` (the machine name the runner declares when it joins) and `token_file` (absolute, or starting with `~/`). Optional: `max_sessions` (1..64, default 2) — how many dispatches loopctl
|
|
507
|
+
| `runner_enroll` | Enroll this machine as a runner (`POST /api/v1/runners`). Required: `name` (the machine name the runner declares when it joins) and `token_file` (absolute, or starting with `~/`). Optional: `max_sessions` (1..64, default 2) — the CEILING on how many dispatches loopctl will ever keep in flight on this machine at once, and the value it starts at. Since runner contract 1.13.0 every join re-applies the machine's own declared `max_sessions` bounded by this one, so a machine may take itself down below its grant and cannot raise itself above it: to carry FEWER sessions change `control.max_sessions` in the runner's own config and reconnect, to carry MORE `runner_revoke` it first and then enrol it again (nothing widens the ceiling in place, and enrolling the same name while its runner is active is a 422; the revoke invalidates the credential, so the new token file must reach the machine and the runner be restarted). The tenant's total across its runners is capped separately by the server. The credential is written to `token_file` with mode 0600 and is **never returned**: the result is only `{ runner: {id, name, inserted_at}, token_file }`, because a tool result lands in the transcript and the token lets its holder join as that machine. The file is created exclusively, so an existing path (or a symlink there) is refused before anything is enrolled; missing parent directories are created 0700. If the token cannot be written after enrollment, the runner is revoked before the error returns. Only a 4xx is treated as a refusal: any other failure (timeout, 5xx, a 2xx that did not parse) may have enrolled the runner, so its response body is never echoed, the runner is revoked when the response proves its id, and otherwise the error points at `runner_list` and `runner_revoke`. 422 (name malformed or taken) and 403 (`custody_tier_required`, `api_key_mint_forbidden`) pass through with their code. |
|
|
508
508
|
| `runner_list` | List enrolled runners (`GET /api/v1/runners`). Optional: `include_revoked`. Enrollment only; connection state is `runner_pool`. |
|
|
509
509
|
| `runner_revoke` | Revoke a runner (`DELETE /api/v1/runners/:id`): its credential stops authenticating and its live socket is disconnected. The undo for `runner_enroll`. Idempotent. Required: `id`. |
|
|
510
|
-
| `runner_pool` | The tenant's connected runners from Presence (`GET /api/v1/runners/pool`): per machine name, `runner_id`, `joined_at`, `draining`, the latest `sample`, `live_sockets` (above 1 means more than one process holds the credential), and the `node` and `machine_id` (Fly Machine) holding the socket. `in_flight` and `max_sessions` are the capacity loopctl holds in Postgres — what dispatch reserves against — and are null only for a runner revoked while its socket drains; `reported_in_flight` and `reported_max_sessions` are the runner's own last report
|
|
511
|
-
| `place_dispatch` | **Place a queued story on a runner** (`POST /api/v1/runners/:runner_id/dispatches`): claims it under a freshly minted custody dispatch and pushes the work. The control-side trigger of the delivery loop. Required: `story_id`, `runner_id` (from `runner_pool`). Optional: `kind` (`implement`, the only kind this tool sends — `triage` is dispatchable on the contract but placement CLAIMS the story, which triage must not do), `dispatch_id` (the idempotency key — generated when absent; pass the SAME one to retry rather than start a second session), `branch`, `base_branch`, `wall_clock_seconds`, `max_turns`. The story object is NOT a parameter: loopctl builds it from its own rows and refuses a caller-supplied one. The story must be `contracted` with its stage at `queued`. Requires `LOOPCTL_USER_KEY`. |
|
|
510
|
+
| `runner_pool` | The tenant's connected runners from Presence (`GET /api/v1/runners/pool`): per machine name, `runner_id`, `joined_at`, `draining`, the latest `sample`, `live_sockets` (above 1 means more than one process holds the credential), and the `node` and `machine_id` (Fly Machine) holding the socket. `in_flight` and `max_sessions` are the capacity loopctl holds in Postgres — what dispatch reserves against — and are null only for a runner revoked while its socket drains; `reported_in_flight` and `reported_max_sessions` are the runner's own last report: `reported_in_flight` is a hint (the runner counts sessions, loopctl counts reservations), while `reported_max_sessions` is what `max_sessions` is copied from on every join (contract 1.13.0), capped at `enrolled_max_sessions` which is also returned. The two can differ in EITHER direction. `max_sessions` BELOW it happens when the machine is declaring above its enrolled ceiling (`enrolled_max_sessions` is what makes that one visible in the payload; the rest are not), when the declaration write has not LANDED — it can fail under lock contention on the runner row and is retried on that socket's 30-second recheck, downward only — or when the socket joined a node older than contract 1.13.0 and has not reconnected since. `max_sessions` ABOVE it happens when the machine declared `0`, held as `1` because the column is 1..64 while this field shows the raw `0`, and again when a declaration write has not landed and the row keeps an older, larger number. A `reported_max_sessions` of 0 means the machine is taking no work and `place_dispatch` refuses a new placement on it, like `draining`. Presence converges only within a cluster. |
|
|
511
|
+
| `place_dispatch` | **Place a queued story on a runner** (`POST /api/v1/runners/:runner_id/dispatches`): claims it under a freshly minted custody dispatch and pushes the work. The control-side trigger of the delivery loop. 409 `runner_declines_work` when the machine declares `draining` or `max_sessions` 0 — refused before the mint, so nothing was claimed; a retry carrying a `dispatch_id` loopctl already holds is re-sent anyway, because its claim is already standing. Required: `story_id`, `runner_id` (from `runner_pool`). Optional: `kind` (`implement`, the only kind this tool sends — `triage` is dispatchable on the contract but placement CLAIMS the story, which triage must not do), `dispatch_id` (the idempotency key — generated when absent; pass the SAME one to retry rather than start a second session), `branch`, `base_branch`, `wall_clock_seconds`, `max_turns`. The story object is NOT a parameter: loopctl builds it from its own rows and refuses a caller-supplied one. The story must be `contracted` with its stage at `queued`. Requires `LOOPCTL_USER_KEY`. |
|
|
512
512
|
| `story_stage` | **Where a story is in the delivery machine** (`GET /api/v1/stories/:id/stage`): stage, `claim_epoch`, `lock_version`, `attempts`, the runner holding it, and the escalation reason when parked. `stage: null` means the loop has never touched it. This is how you watch a run, and the only way to see why a stage report was refused `stale_stage`. `escalation_reason` is untrusted session text. Required: `story_id`. |
|
|
513
513
|
| `resolve_escalation` | **Move an escalated story off `escalated`, as a human** (`POST /api/v1/stories/:id/stage/resolve`): `to` is `queued` (work it again), `done` or `failed`. The other half of `escalate_story` — without it a parked story stays parked for ever. Requires `LOOPCTL_USER_KEY` on a key no dispatch minted: a session cannot resolve the escalation it raised. Optional: `reason`. |
|
|
514
514
|
| `force_unclaim_story` | **Take a story back from the agent holding it** (`POST /api/v1/stories/:id/force-unclaim`). Resets it to `agent_status: pending` with `assigned_agent_id` cleared AND makes the delivery stage row follow the release back to `queued`. **It frees the STAGE; it does not make the story placeable.** `place_dispatch` wants `contracted` + `queued` (see its own row in this table), and the release leaves the story at `pending`, whose only transition is `pending -> contracted` — so running `place_dispatch` next answers the identical 409 `invalid_transition`. The remedy is three calls, in this order: `force_unclaim_story`, then `contract_story`, then `place_dispatch`. A story parked at `claimed` with nobody on it is the residue of a compensation that did not complete, not what a refused dispatch normally leaves: placement releases the claim inline when a runner refuses, and the claim lease releases it unattended once `claimed_until` passes — reach for this to get it back now, or when both of those left it held. Requeues from any stage a claim holds; a stage no claim holds keeps its stage and is rebound to the new epoch; `done` and `failed` are untouched. For an ESCALATED story use `resolve_escalation` instead — that one releases AND re-contracts, so its `queued` really is placeable. Requires an orchestrator-ROLE key: the action is `exact_role: :orchestrator`, so a user or superadmin key is 403'd like any other non-member. Put it in `LOOPCTL_ORCH_KEY` (which is then the key sent, and a global `LOOPCTL_API_KEY` does not displace it) or, with no orchestrator key set, in `LOOPCTL_API_KEY`. The orchestrator key must be linked to a registered agent (400 otherwise) and the tenant must be human-anchored (403 `custody_tier_required` otherwise). Does not touch `verified_status`. Safe to run twice: on an already-pending story the stage row is written only if it is stranded behind the story's claim epoch or still in flight. No request body. Required: `story_id` (refused locally if it is not a UUID). |
|
package/index.js
CHANGED
|
@@ -7877,8 +7877,18 @@ const TOOLS = [
|
|
|
7877
7877
|
minimum: 1,
|
|
7878
7878
|
maximum: 64,
|
|
7879
7879
|
description:
|
|
7880
|
-
"
|
|
7881
|
-
"
|
|
7880
|
+
"The CEILING on how many dispatches loopctl will ever keep in flight on this " +
|
|
7881
|
+
"machine at once (default 2), and the value it starts at. Since runner contract " +
|
|
7882
|
+
"1.13.0 every join re-applies the machine's own declared max_sessions BOUNDED BY " +
|
|
7883
|
+
"this one, so a machine may take itself down below its grant and cannot raise " +
|
|
7884
|
+
"itself above it. To make a machine carry FEWER sessions, change " +
|
|
7885
|
+
"control.max_sessions in the runner's own config and reconnect it — no call here " +
|
|
7886
|
+
"is needed. To let it carry MORE than its grant, runner_revoke it FIRST and then " +
|
|
7887
|
+
"enrol it again: nothing widens this in place, and enrolling the same machine name " +
|
|
7888
|
+
"while its current runner is active is a 422. The revoke invalidates the " +
|
|
7889
|
+
"credential, so the new token_file must reach the machine and the runner be " +
|
|
7890
|
+
"restarted. The tenant's total across all its runners is capped separately by " +
|
|
7891
|
+
"the server.",
|
|
7882
7892
|
},
|
|
7883
7893
|
},
|
|
7884
7894
|
required: ["name", "token_file"],
|
|
@@ -7922,7 +7932,23 @@ const TOOLS = [
|
|
|
7922
7932
|
"and machine_id (Fly Machine) holding the socket. in_flight and max_sessions are the " +
|
|
7923
7933
|
"CAPACITY loopctl holds in Postgres — the slots dispatch reserves against — and are null " +
|
|
7924
7934
|
"only for a runner revoked while its socket drains; reported_in_flight and " +
|
|
7925
|
-
"reported_max_sessions are what the runner itself last reported
|
|
7935
|
+
"reported_max_sessions are what the runner itself last reported. reported_in_flight is a " +
|
|
7936
|
+
"hint (the runner counts sessions, loopctl counts reservations); reported_max_sessions is " +
|
|
7937
|
+
"NOT — since contract 1.13.0 loopctl copies it into max_sessions on every join, capped at " +
|
|
7938
|
+
"the runner's enrolled_max_sessions, which is also returned. The two can differ in EITHER " +
|
|
7939
|
+
"direction, so read a difference rather than assuming it. max_sessions BELOW " +
|
|
7940
|
+
"reported_max_sessions happens when the machine is declaring above the ceiling it was " +
|
|
7941
|
+
"enrolled with (enrolled_max_sessions is what makes that one visible in the payload; the " +
|
|
7942
|
+
"rest are not), when the declaration write has not LANDED — it can fail under lock " +
|
|
7943
|
+
"contention " +
|
|
7944
|
+
"on the runner row and is retried on that socket's 30-second recheck, downward only — or " +
|
|
7945
|
+
"when the socket joined a node older than contract 1.13.0 and has not reconnected since. " +
|
|
7946
|
+
"max_sessions ABOVE reported_max_sessions happens when the machine declared 0, which the " +
|
|
7947
|
+
"1..64 column holds as 1 while this field shows the raw 0, and again when a declaration " +
|
|
7948
|
+
"write has not landed and the row keeps an older, larger number. A reported_max_sessions " +
|
|
7949
|
+
"of 0 means the machine is taking no work and place_dispatch refuses a new placement on " +
|
|
7950
|
+
"it, like draining. " +
|
|
7951
|
+
"live_sockets " +
|
|
7926
7952
|
"above 1 means more than one process holds that runner's credential. A killed runner " +
|
|
7927
7953
|
"disappears once its socket closes. Presence converges only " +
|
|
7928
7954
|
"within a cluster, so on an unclustered multi-node deployment a runner on another node is " +
|
|
@@ -7948,7 +7974,14 @@ const TOOLS = [
|
|
|
7948
7974
|
"IDEMPOTENT on `dispatch_id`, which is generated for you unless you pass one — pass the " +
|
|
7949
7975
|
"SAME id to retry a call that timed out, or you start a second session on the same " +
|
|
7950
7976
|
"story. Requires LOOPCTL_USER_KEY: only an unlineaged user key may root the custody " +
|
|
7951
|
-
"lineage this mints
|
|
7977
|
+
"lineage this mints.\n\n" +
|
|
7978
|
+
"409 `runner_declines_work` means the machine's live socket declares `draining` (or a " +
|
|
7979
|
+
"max_sessions of 0, which says the same thing). NOTHING WAS CLAIMED — it is refused " +
|
|
7980
|
+
"before the mint — so place on another runner rather than repairing anything, with a " +
|
|
7981
|
+
"NEW dispatch_id. Read `draining` and `reported_max_sessions` from runner_pool before " +
|
|
7982
|
+
"choosing a machine. It refuses a NEW placement only: retrying with a dispatch_id " +
|
|
7983
|
+
"loopctl already holds still re-sends that dispatch, because its claim is already " +
|
|
7984
|
+
"standing and a draining machine is asked to finish what it holds, not to take more.",
|
|
7952
7985
|
inputSchema: {
|
|
7953
7986
|
type: "object",
|
|
7954
7987
|
properties: {
|