Adds an opt-in hard per-session memory ceiling on top of patch #9's scoped-OOM.
The exec already ships the full OCI Spec, so the limit rides
spec.linux.resources.memory.limit — no RPC/protobuf change:
- host framework: LinuxProcessConfiguration.memoryLimitInBytes; LinuxContainer.exec
stamps it onto the exec spec.
- guest: Server+GRPC.createProcess reads it back and applies it as the exec
cgroup's memory.max (new Cgroup2Manager.setMemoryMax) via createExec/ManagedProcess.
- Nucleic: ContainerServiceSettings.controlPerSessionMemoryGiB (default 0 = off),
applied only to the shared control container (ContainerManager.exec); wired
through ContainerEngine.exec.
So one session can't consume the whole shared container's memory before its own
(oom.group-scoped) OOM. Default off preserves #9's behavior. Compile-verified host
+ musl guest; rides the pending -nucleic2 image, still runtime-pending.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The swiftly toolchain used by the vminit-image CI resolves Foundation/
Virtualization but not the 'os' overlay, so 'import os' failed with
"no such module 'os'". Guarding the diagnostic logger degrades it to a
no-op under such toolchains while local (Xcode) builds keep it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Host-side (ships with a normal swift build):
- LinuxProcess: non-blocking stdio relay (O_NONBLOCK + nucleicDrainNonBlocking)
so a wedged stream can't head-of-line-block sibling execs' relays; atomic
stdio-or-abort start (patches #5, #6).
- Vminitd: bounded deleteProcess timeout so teardown can't hang a wedged
channel (patch #7).
- ContainerizedProcessHandle: call LinuxProcess.delete() after exit and on
force-close — fixes a per-turn leak (per-exec vsock/gRPC connection +
runConnections() task) in the long-lived shared control container. Likely
the "degrades until app restart" root cause.
- ClaudeCodeBackend: map the atomic-start abort to a recoverable AgentError so
a failed launch settles as retryable instead of locking the composer.
Guest-side (rides the custom vminitd initfs; inert until the image is built):
- ManagedProcess: offload the blocking start off the gRPC event loop (patch #8).
- Per-exec cgroups (patch #9) recorded as design only — cross-cutting.
Pipeline:
- .github/workflows/vminit-image.yml builds vminitd from the vendored source
and pushes ghcr.io/abkslm/vminit; ContainerEngine.vminitReference repointed
at the custom image.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Containerized control sessions could hang with no output ("Working…" forever): the agent's exec
stdio over the vminitd vsock channel intermittently failed to carry bytes, so claude ran and
reached the approval server but its stdin/stdout never connected — it idled in interactive
stream-json mode and never exited. Forensics (nucleic.sqlite + per-session claude-home MCP logs)
showed every stdio layer byte-identical to a working state, i.e. a flaky framework race, not a
regression in our code. Make the failure recoverable and visible instead of an eternal spinner:
- ClaudeCodeBackend: a 60s spawn watchdog on containerized runs — no first stdout → emit a
recoverable error (with guest stderr + container probe), SIGKILL the wedged process, and finish
the run errored, instead of awaiting stdoutLines forever.
- Stop reliability: ProcessHandle.forceCloseStreams() (ContainerizedProcessHandle finishes its
line streams host-side; default no-op for the host pipe handle), wired into every kill
escalation (terminate/interruptThenKill/killGroupAfter) so a force-killed run always settles
even when the guest wait/stdio RPC wedges — the real cause of "Stop is inconsistent".
- Cap MCP_TIMEOUT (connection) to 60s so an unreachable approval server can't wedge startup for
~24.8 days; MCP_TOOL_TIMEOUT stays unbounded for human-answered approvals.
- LinuxProcess.setupIO logs which stdio stream fails to connect (os.Logger, com.nucleic /
container-io) so a stall pinpoints the failing stream. Vendored patch #3.
- Tests: stop-escalation force-close, responsive-process no-op, MCP timeout asymmetry.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Root-cause and fix the four reported container regressions plus two
adjacent confirmed bugs.
- Memory balloon (CPU 100% + output freeze that never recovered): the
autoballoon drove the whole-VM target from a per-container cgroup figure
with no guest swap, spinning a swapless guest in perpetual direct
reclaim. Default memoryManagement to off; make the target whole-VM-aware
(reserveBytes) so it never inflates below the working set plus the
guest's non-cgroup footprint; deflate the balloon on a failed stats read
instead of freezing it inflated.
- Stop button: signal the agent's whole process group (new vendored
LinuxProcess.killProcessGroup, negative pid) so forked children die too;
replace the unbounded wait() in every teardown/shutdown with a bounded
terminate() that escalates SIGTERM -> SIGKILL; interrupt escalates to a
group kill so a wedged agent always stops.
- MCPApprovalServer port-0 race: single-flight start(host:) so concurrent
sessions sharing one control-container server all receive the real bound
port; publish listener+port only after .ready (a failed bind no longer
pins a stale port 0); guard the Claude call site against port 0.
- Container CPU metric: divide the CPU delta by the actual measured window
instead of a fixed 200 ms, so it stops over-reading under load.
- Command interceptor: drop the ~40 coreutil Node shims so cat/grep/etc
run their native binaries (no Node-per-command); the bash tracer still
records them as metadata.
- Make the bash command tracer opt-in (commandTracingEnabled, default
off) — the per-command DEBUG trap only activates when enabled; the
git/gh interception the conflict/merge system relies on stays always-on.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Switch the containerization dependency from the github URL to a vendored copy
(third_party/containerization, upstream commit 6b7b42ca) referenced by path, so
we can carry a small local patch that upstream lacks: LinuxContainer.Configuration
gains a `vmExtensions` field forwarded into VMConfiguration.extensions. Upstream
already supports VMConfiguration.extensions + the VZInstanceExtension hook, but
LinuxContainer — our only entry point — never forwarded them, so there was no way
to attach a device (e.g. a memory balloon) to a container's VM.
Tests/, docs/, examples/, images/ and the corresponding test targets are trimmed
for footprint (we never build the dependency's tests). See PATCHES.md for the full
diff vs. upstream and the re-vendoring procedure. Also adds the ContainerizationExtras
product to NucleicCore (AddressAllocator, named in the configureVZ signature).
Co-Authored-By: Claude Opus 4.8 <[email protected]>