Summary
Every supervised module and every process a module spawns lives in ck-subc.service's single cgroup. On 2026-09-07 an aft tool shell's runaway (12 orphaned vitest workers, ~32 GiB swapped) sat in that cgroup, took the box to load 86 / memory PSI 84%, and was one systemd-oomd decision away from killing the daemon and all four modules — which is how the Aug-10 OOM went. Design question, not an ask-to-build: should the daemon place each supervised child in its own cgroup subtree so a module's runaway is bounded and killable without touching the daemon?
Live facts (icebox, daemon 0.17.20)
systemctl --user show ck-subc -p Delegate -p KillMode -p ManagedOOMMemoryPressure
Delegate=no
KillMode=control-group
ManagedOOMMemoryPressure=auto # inherits app.slice
cgroup.subtree_control: (empty)
Flat cgroup. systemd-oomd selects its kill candidate among the descendants of the monitored slice; with no descendants under ck-subc.service, the candidate is the service itself. Kill granularity is the problem, not a memory cap.
What #16 already ruled, and why this is narrower
#16 ruled that core stays portable and does not carry box-vitals or memory-kill policy. This proposal is placement only: a Linux cfg island that writes the child's pid into a per-module cgroup at spawn, a no-op elsewhere, no wire change, no policy in core. Limits stay with the operator (unit drop-in or per-module config later, if ever).
Two halves — the daemon owns one
Tool shells are AFT's half. aft spawns them; it can spawn each into its own transient scope (systemd-run --user --scope --collect … on Linux, plain spawn elsewhere). The shell leaves ck-subc.service's cgroup and oomd kills the scope, never the daemon or aft. A per-module subtree cannot solve this case: aft's tool children would sit with aft, so killing the runaway kills aft's connection too. That half fixes the 09-07 incident by itself and is routed to aft separately.
A module's own runaway is the daemon's half. Per-module subtree:
Placement is the ruling I need
Either of:
supervise.rs, behind #[cfg(target_os = "linux")], with a config flag defaulting off until the unit has Delegate=yes (writing cgroup.procs without delegation is EACCES; the daemon must degrade to today's behaviour, loudly once, not fail spawn).
- A launcher wrapper outside core (the daemon spawns
ck-launch <module> which does the cgroup write then execs), keeping core free of cgroup knowledge at the cost of one more hop in the process tree and a second binary to attest.
I lean (1) for the child.wait() and provenance reasons, and will build whichever you rule. Explicit non-goals: no memory.high policy in core, no box-vitals surface, no behaviour change when delegation is absent.
Summary
Every supervised module and every process a module spawns lives in
ck-subc.service's single cgroup. On 2026-09-07 an aft tool shell's runaway (12 orphaned vitest workers, ~32 GiB swapped) sat in that cgroup, took the box to load 86 / memory PSI 84%, and was one systemd-oomd decision away from killing the daemon and all four modules — which is how the Aug-10 OOM went. Design question, not an ask-to-build: should the daemon place each supervised child in its own cgroup subtree so a module's runaway is bounded and killable without touching the daemon?Live facts (icebox, daemon 0.17.20)
Flat cgroup. systemd-oomd selects its kill candidate among the descendants of the monitored slice; with no descendants under
ck-subc.service, the candidate is the service itself. Kill granularity is the problem, not a memory cap.What #16 already ruled, and why this is narrower
#16 ruled that core stays portable and does not carry box-vitals or memory-kill policy. This proposal is placement only: a Linux
cfgisland that writes the child's pid into a per-module cgroup at spawn, a no-op elsewhere, no wire change, no policy in core. Limits stay with the operator (unit drop-in or per-module config later, if ever).Two halves — the daemon owns one
Tool shells are AFT's half. aft spawns them; it can spawn each into its own transient scope (
systemd-run --user --scope --collect …on Linux, plain spawn elsewhere). The shell leavesck-subc.service's cgroup and oomd kills the scope, never the daemon or aft. A per-module subtree cannot solve this case: aft's tool children would sit with aft, so killing the runaway kills aft's connection too. That half fixes the 09-07 incident by itself and is routed to aft separately.A module's own runaway is the daemon's half. Per-module subtree:
Delegate=yes(+DelegateSubgroup=so the daemon's own threads sit in a leaf and the no-internal-processes rule holds).<service-cgroup>/<module_id>/and writes the child pid to itscgroup.procsin apre_exechook, so the child never runs a line outside its cgroup.child.wait()still works — the child stays a direct child. This is the difference from wrapping the spawn insystemd-run --scope, which breaks the direct-parent assumption flagged on Supervisor has no per-module resource visibility or bounds — one module's allocator hoard killed the entire daemon cgroup #16.cgroup.killtakes the module and its children as one unit;memory.currenton the same node gives the per-module RSS that Supervisor has no per-module resource visibility or bounds — one module's allocator hoard killed the entire daemon cgroup #16 asked for, for free.Placement is the ruling I need
Either of:
supervise.rs, behind#[cfg(target_os = "linux")], with a config flag defaulting off until the unit hasDelegate=yes(writingcgroup.procswithout delegation isEACCES; the daemon must degrade to today's behaviour, loudly once, not fail spawn).ck-launch <module>which does the cgroup write thenexecs), keeping core free of cgroup knowledge at the cost of one more hop in the process tree and a second binary to attest.I lean (1) for the
child.wait()and provenance reasons, and will build whichever you rule. Explicit non-goals: nomemory.highpolicy in core, no box-vitals surface, no behaviour change when delegation is absent.