docs(platform): design self-host stack supervision — keep pgid, decline Quadlet (RIG-3239) - #872
Draft
rigel-mintaka wants to merge 6 commits into
Draft
docs(platform): design self-host stack supervision — keep pgid, decline Quadlet (RIG-3239)#872rigel-mintaka wants to merge 6 commits into
rigel-mintaka wants to merge 6 commits into
Conversation
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
Compass engineering docs preview: https://compass-native-rig-3239-stac.compass-eng-docs.pages.dev Deployed from Changed pages: |
…ne Quadlet (RIG-3239) Decides how the long-lived self-host stack services (compass-server, compass-runner, containerized postgres, OTel collector) are supervised. Ruling (Matt fork — OQ-1): KEEP the hand-rolled DL-183/DL-262 pgid mechanism as the single built-in supervision model; do NOT adopt per-service Podman Quadlet units. Quadlet is Linux/systemd-only, so the pgid path survives for the embedded + non-systemd tiers regardless — adopting Quadlet means two behaviorally-equivalent supervision models, the imperative cold sequence would need oneshot pre-units + sdnotify re-plumbing, and per-service units displace the DL-259-named `compass-stack up` verb. Optionally ship a documented thin systemd USER-unit wrapper (Type=oneshot + RemainAfterExit=yes) for boot-start/logout survival — systemd wrapping the supervisor, never replacing it. Docker-socket declined (daemon model vs the rootless/no-daemon invariant). The record was red-teamed by a design-critic pass before submission; its findings are folded: the "one model everywhere including macOS" claim corrected to "one portable model with a named unbuilt darwin start-time-seam port" (pgidfile.go's /proc reader is Linux-only, `up` refuses on darwin today); the crux argued on maintenance cost not slice size (DL-259 pins the whole self-host tier to Linux); the oneshot wrapper's latent failure modes (status-lie under RemainAfterExit, stop-what-you-didn't-start, PATH/linger/After) folded into OQ-4 + the T1 unit checklist; and the crash-recovery fork reshaped into OQ-3 as three options — accept the gap (v1), a blocking `compass-stack up --supervise` under Type=exec/Restart=on-failure (systemd whole-stack recovery with one model), or reopen Quadlet. Three load-bearing Matt forks ride to the design gate: OQ-1 (adopt Quadlet vs keep), OQ-2 (ship the T1 wrapper at all), OQ-3 (crash-recovery posture). Ledger: adds DL-328 (Infrastructure & CI section). Status stays Draft — freezes Active on merge. Refs RIG-3239 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ed-team (RIG-3239) Re-author the self-host stack supervision record against Matt's RIG-3258 ruling and fold the design-critic red-team on the result. The original draft weighed Quadlet-vs-pgid and proposed accepting the crash-recovery gap for v1; Matt ruled: ship a one-command constant-on service on ALL platforms with auto-start at reboot and REAL crash recovery. Ruling folded into the record: - Keep the DL-183/DL-262 pgid mechanism as the SINGLE cross-platform model; decline Quadlet on a third independent ground (Linux/systemd-only — structurally cannot meet the all-platforms bar). - Whole-stack crash recovery = a blocking `compass-stack up --supervise` foreground mode wrapped by the platform-native OS supervisor's restart policy (systemd `Type=exec` on Linux, launchd `KeepAlive` on macOS). - One-command `compass-stack service install`/`uninstall` writing + enabling the native unit; the OS supervisor supplies only restart/backoff/boot; the pgid mechanism stays the sole bring-up/teardown and status truth stays `compass-stack status`. Red-team corrections folded (core direction survived every attack): - T1: the supervise loop is NOT a free consumer of the stack core. Name the bounded core change it forces — `Process.Wait` is single-caller (a fan-in Wait plus drainChildren's Wait double-calls exec.Cmd.Wait), so the loop takes sole Wait ownership; container children's Wait is 120s-bounded (supervise polls their liveness instead of a restart-storm every ~2min); Wait's cancel path group-SIGKILLs (teardown on a non-cancelable ctx, the upLocked WithoutCancel pattern); a partial drain orphans a survivor across restart (pre-spawn record-consuming cleanup). - T2: pin `KillMode=mixed` / `AbandonProcessGroup=true` so the OS does not parallel-kill the children before the DL-183 ordered drain; pin `RestartSec`/`ThrottleInterval` + an explicit start-limit posture; drop the per-user-manager-ignored `After=network.target` (loopback bind); make the `down` verb unit-aware so it stops through the unit rather than being undone by a restart. - T3: the darwin identity reader is a TWO-site swap (spawn-side pgidfile seam AND down-side groupsignal reader) sharing one uint64 timeval encoding, with the existing mirror-test extended so they cannot drift. - OQ-5: fold the launchd crashloop cost into option (a) — an experimental label does not stop a KeepAlive crashloop, so (a) carries an install-time preflight + a bring-up-failure self-limit. Routed to Matt as RIG-3261. Ledger-impact: DL-328 rewritten to the ruled design (keep pgid, decline Quadlet, ship --supervise + service install), Status Active. Spec-impact: none Refs RIG-3239 Refs RIG-3261 Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka
force-pushed
the
compass-native/rig-3239-stack-supervision-design
branch
from
September 4, 2026 21:13
7dd3688 to
eb3ef67
Compare
…y (RIG-3239) Fold the single-review-of-record (ReviewStackSupervision872) on the stack-supervision design: 1 high, 5 medium, 4 low. All load-bearing Go citations were re-verified at the PR head before folding. High (lock-lifetime self-deadlock) — a genuine load-bearing fork on the Matt-ruled DL-183 interlock, not a coordinator ruling. `up --supervise` is the first caller to hold the state-dir lockfile with a live pid for the stack's whole lifetime, so `DownDetached`'s live-holder guard (`downdetached.go:74-84`) permanently refuses `down` and T1's pre-spawn sweep self-refuses. Folded the two factual defects it exposed (the false T2 stop-truth premise; the T1 item-4 ordering) and captured the mechanism as OQ-6 (release-at-Ready vs mode-token guard), recommendation (b), routed to Matt via RIG-3261 alongside OQ-5. Added a Global Constraints lock-lifetime invariant. Medium — OQ-4 `After=network.target` contradiction removed to match the ruled position; `main.go:30-31` miscite corrected to `main.go:42-45` (`defaultListenAddr`); Approach reason-#1 and OQ-5 darwin-port sites widened from one-seam to two-seam; T1 container-supervision fork ruled (liveness poll via `ContainerController.Exists`, unbounded-`podman wait` rejected) and the Interfaces `Health`/`Exists` roles split; `### Resolved decisions` promoted to `##` so the Open-Questions freeze scope holds only OQ-4/OQ-5/OQ-6. Low — OQ-5/OQ-6 routed to RIG-3261 in the headings; `golang.org/x/sys` promotion indirect→direct noted with the `go.mod`/`go.sum` delta added to T3 Produces; DL-328 row relocated from `## Infrastructure & CI` to `## Desktop shell` (placement only, row text byte-identical); garbled "non-systemd-agnostic" Alternatives prose fixed. Additive review-fix commit atop the PR head; no `--supervise` mechanism was ruled — OQ-6 is Matt's. Ledger-impact: DL-328 relocated within DECISIONS.md (no text/status change) Refs RIG-3239 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…nsistency (RIG-3239)
ReviewStackSupervision872r2 confirmed all 10 prior findings closed and the
freeze gate PASS, but flagged three new mediums of the summary-vs-detail
drift class: the prior fold corrected the OQ-6 lock-lifetime premise at its
detail sites (T1 item 4, T2 stop-truth, Global Constraints) but left two
upstream Approach summaries and one T2->T1 cross-reference carrying the
pre-fold, now-falsified claims. Fold reconciles them:
- Approach :340 counted "the one remaining fork" (OQ-5) while Open Questions
now holds two load-bearing forks; names both OQ-5 and OQ-6.
- Approach :283-285 justified unit-aware `down` by a "undone by a restart"
rationale the T2 fold already retracted; rewritten to the live-holder-guard
model and flags the OQ-6 dependency.
- T2 stop-truth :648 delegated the OQ-6 behavior to "T1's smoke", which had no
such leg; added T1 test-cycle item (5) (`down` against a supervised stack
exits ZERO, never `ErrStackStarting`) and repointed the cross-ref.
- L1: T4 T3 tasks row still singular ("readStartTime"); widened to both
darwin seams (`readStartTime` + `readGroupLeaderStartTime`, one shared
encoding), matching the two-seam prose.
L2 (two ragged line-wraps, render-correct, no dropped text) deferred — not
churning a freeze-ready record for cosmetics.
Additive fold child off the bookmark tip; no DECISIONS.md change (DL-328
unchanged). Gates: markdownlint-cli2 0 errors (192 files); design-ledger-gate
OK (292 rows / 122 headers).
Refs RIG-3239
Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ation + arity (RIG-3239) ReviewStackSupervision872r3 confirmed all 3 prior mediums + L1 genuinely closed at both detail and summary sites, but caught two live gating mediums of the recurring summary-vs-detail drift class in the freeze candidate: - M1 (new, from the r2 fold): T1 test-cycle item (5) stated an unconditional "exits ZERO", true only under OQ-6 option (b), contradicting the option-discriminating T2 stop-truth bullet and T1's own child-death non-zero exit contract. Rewrote item (5) to be option-agnostic — never wedges on ErrStackStarting and the stop lands with the ruling; under (b) the supervising holder is signalled and exits ZERO, under (a) the lock is released so down signals the children and the child-death path applies. Anchored it in-process to the OQ-6 counterpart of downdetached_test.go:238 (verified at source). Being option-agnostic, it no longer belongs in the OQ-6 (b)-dependent inventory (:854), closing M1's second site. - M2 (pre-existing, survived all three folds): the bolded "three interacting contract facts" summary counted a four-item list; item 4 (pre-spawn survivor cleanup) is the most cross-referenced fact (Global Constraint + OQ-6-(b) site), so the miscount is load-bearing. Fixed the count to four. - L1 (folded into M1's edit): item (5) now disambiguates the in-process harness assertion from the Linux process smoke. Gates: markdownlint-cli2 0 errors (192 files); design-ledger-gate OK (292 rows / 122 headers). Diff: design.md only, 11 insertions / 7 deletions. Ledger-impact: none (DL-328 cell unchanged) Refs RIG-3239 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ory + reason ordinal (RIG-3239) ReviewStackSupervision872r4 confirmed the r3 fold clean (M1 item-(5) option-discrimination, M2 arity 3->4, L1 harness-vs-process all genuinely closed at detail AND summary sites; no fold-introduced contradiction; freeze gate PASS; new downdetached_test.go:238 citation verified reachable at source), and caught two pre-existing instances of the summary-vs-detail drift class the fold never touched: - Medium (:858-859): the OQ-6 Recommendation's "if Matt rules (a), rewrite these" inventory named T1 item 4 and the T2 stop-truth bullet as (b)-dependent; neither is. T1 item 4's own parenthetical (:528-530) names (b) as the DECLINED alternative and calls the before-lock sweep "the option-independent fix", and Global Constraint :439-442 mandates the sweep "Whichever OQ-6 option lands" as a hard MUST — so the inventory instructed an executor who receives ruling (a) to fold away a requirement that survives (a) unchanged. The T2 bullet (:645-648) is option-discriminating and already carries both (a)/(b) branches. Second-order: the r3 fold made item (5) option-agnostic too, so the record now has ZERO genuinely (b)-dependent sites -> rewrote the sentence to state every T1/T2 site is option-agnostic and a ruling of (a) needs only deletion of moot (b) branches, not a rewrite (per the reviewer's suggested fix; NOT re-listed, which would re-falsify the M1 fix). - Low (:178-179): the first "keep pgid" reason called itself a "second, independent reason on top of the two below" (1 on top of 2 = third); the Resolved-decisions row for the same argument (:867) already says "third". Aligned to "third". Both folded coordinator-direct (judgment-dense frozen-record consistency). Gates: markdownlint-cli2 0 errors (192 files); design-ledger-gate OK (292 rows / 122 headers). Diff: design.md only, 9 ins / 3 del. Refs RIG-3239 Ledger-impact: none (DECISIONS.md unchanged) Co-authored-by: Matt Wilkinson <matt@rigel.build>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Decides how the long-lived self-host stack services (compass-server,
compass-runner, containerized postgres, OTel collector) are supervised.
Ruling (Matt fork — OQ-1): KEEP the hand-rolled DL-183/DL-262 pgid mechanism
as the single built-in supervision model; do NOT adopt per-service Podman
Quadlet units. Quadlet is Linux/systemd-only, so the pgid path survives for the
embedded + non-systemd tiers regardless — adopting Quadlet means two
behaviorally-equivalent supervision models, the imperative cold sequence would
need oneshot pre-units + sdnotify re-plumbing, and per-service units displace
the DL-259-named
compass-stack upverb. Optionally ship a documented thinsystemd USER-unit wrapper (Type=oneshot + RemainAfterExit=yes) for
boot-start/logout survival — systemd wrapping the supervisor, never replacing
it. Docker-socket declined (daemon model vs the rootless/no-daemon invariant).
The record was red-teamed by a design-critic pass before submission; its
findings are folded: the "one model everywhere including macOS" claim corrected
to "one portable model with a named unbuilt darwin start-time-seam port"
(pgidfile.go's /proc reader is Linux-only,
uprefuses on darwin today); thecrux argued on maintenance cost not slice size (DL-259 pins the whole self-host
tier to Linux); the oneshot wrapper's latent failure modes (status-lie under
RemainAfterExit, stop-what-you-didn't-start, PATH/linger/After) folded into
OQ-4 + the T1 unit checklist; and the crash-recovery fork reshaped into OQ-3 as
three options — accept the gap (v1), a blocking
compass-stack up --superviseunder Type=exec/Restart=on-failure (systemd whole-stack recovery with one
model), or reopen Quadlet.
Three load-bearing Matt forks ride to the design gate: OQ-1 (adopt Quadlet vs
keep), OQ-2 (ship the T1 wrapper at all), OQ-3 (crash-recovery posture).
Ledger: adds DL-328 (Infrastructure & CI section). Status stays Draft — freezes
Active on merge.
Refs RIG-3239
Co-authored-by: Matt Wilkinson matt@rigel.build