Skip to content

docs(platform): design self-host stack supervision — keep pgid, decline Quadlet (RIG-3239) - #872

Draft
rigel-mintaka wants to merge 6 commits into
mainfrom
compass-native/rig-3239-stack-supervision-design
Draft

docs(platform): design self-host stack supervision — keep pgid, decline Quadlet (RIG-3239)#872
rigel-mintaka wants to merge 6 commits into
mainfrom
compass-native/rig-3239-stack-supervision-design

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

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

@linear-code

linear-code Bot commented Sep 4, 2026

Copy link
Copy Markdown

RIG-3239

@trunk-io

trunk-io Bot commented Sep 4, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

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

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-native-rig-3239-stac.compass-eng-docs.pages.dev

Deployed from compass-native/rig-3239-stack-supervision-design at 1e9cd09.

Changed pages:

rigel-mintaka and others added 2 commits September 4, 2026 17:12
…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
rigel-mintaka force-pushed the compass-native/rig-3239-stack-supervision-design branch from 7dd3688 to eb3ef67 Compare September 4, 2026 21:13
rigel-mintaka and others added 4 commits September 4, 2026 18:28
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant