Compose capacity
Status: Implementation merged; release, deployment and remote acceptance unverified. This note is proposed documentation; its live publication has not been verified. Tracking: Master #247, compose issue #22, merged implementation PR #24. No Ge review or testing status is asserted.
2026-09-12 — count convergence and safe local recovery
Compose retains daemon groups with an explicit non-negative count setpoint.
Lowering four to two terminates confirmed surplus through the provider endpoint;
zero retires a group. Pending or uncertain workers occupy launch slots but cannot
justify terminating a healthy survivor. Victims are selected by exact project and
numeric-suffix group ownership, unhealthy first, then newest creation time and
higher worker ID. Gone/stopping history no longer counts as capacity.
Queue elasticity was not substituted for compose: it owns queue membership rather than project/group identity, does not preserve the complete group launch template, and its reviewed production scale-down path deletes worker rows without provider termination. Keep queue elasticity fixed for compose-owned fleets.
The follow-up adds a local-user lock shared by up and down, independent of
compose file path and selected group. The key includes server URL, authenticated
namespace and project. A journal records each mutating intent before dispatch.
Failed or ambiguous requests and process death retain the lock, blocking blind
retries. No PID/age-based stale-lock takeover is performed. Requests already in
flight may finish; new mutations stop once uncertainty is observed.
Both commands reject a compose/auth namespace mismatch before network access.
down --terminate now reports provider failure, ignores foreign-project workers
and already-stopping hosts, and up --wait returns nonzero on startup timeout.
These are host/daemon capacity operations, separate from user-space jobs. This
work does not change nymph job supervision or vault SSH sync.
Validation and merge evidence
At tested implementation head adb528673bc98cc68a989c63e314bd237eb6db86:
- TypeScript build passed.
- All 44 compose/locking regressions passed.
- A clean
git archive HEADexport, excluding unrelated dirty source, passednpm test: 641 passed, one skipped, zero failed; type checking is included. - Tests cover four-to-two shrink, zero count, retained history, pending survivors, group ownership/order, concurrent local processes, SIGKILL during pending intent, lost launch responses, failed termination with a missing CP row, namespace mismatch, explicit recovery and startup timeout.
PR #24 merged as eec3335f7ef7a3918a7e49b7cef4f2e9d7a3ada9 at
2026-09-13 00:50:13 UTC (2026-09-12 Pacific). The tests above apply to the tested
PR head; they are not a deployed-system acceptance claim. Provider calls used
synthetic HTTP fixtures; the process-crash test used a local child process.
Detailed readiness evidence.
Recovery and remaining gaps
Locks live under $XDG_CONFIG_HOME/lakeshore/compose-locks, defaulting to
~/.config/lakeshore/compose-locks. To recover, ensure the original writer has
stopped, inspect journal.json, and reconcile every recorded outcome with the
control plane and provider, including resources whose worker row disappeared.
Only then remove the specific lock directory manually and run up --plan.
Do not delete a live writer's lock.
- Different hosts, users, config roots, server aliases, older clients and other controllers do not share this lock. Distributed leases, fencing, idempotency and atomic reconciliation still need control-plane work; serialize externally.
- Automatic crash/wedge reaping and retry/backoff remain outside this one-shot command. Pending/unknown workers retain occupancy. Power-loss recovery was not tested.
- Some CP versions delete worker rows despite provider termination failure. Local journals block blind retries but do not restore missing ownership or roll back cloud actions.
- Termination can interrupt running jobs; compose does not manage their lifecycle.
- Real-provider acceptance, package release, deployment and live verification remain unverified. See Unreleased release notes.
Future coherent changes to this note require an Unreleased update and a Slack DM to Ge with source links until publication is verified. Sent-message evidence is recorded in issue #22 and PR #24, separately from implementation and delivery status.