Commit Graph
16 Commits
Author SHA1 Message Date
abkslmandClaude Fable 5 e90b405da0 Deep-sweep fixes: cooperative-pool starvation, stdio tail loss, epoll integrity, UI pins
Host — the two remaining app-wide stall mechanisms plus main-thread pins
found by mining all nine hang reports:
- LinuxProcess.startStdinRelay wrote to a BLOCKING stdin fd on a
  width-limited cooperative-pool thread, non-cancellably; wedged guests
  starved the whole concurrency runtime (decode loops, watchdogs — an
  app-wide freeze surviving the reconcile fix). Writes now offload to a
  per-process GCD queue (vendored patch #18).
- TranscriptWriter (actor) did blocking write/fsync on the cooperative
  pool; it now runs on its own DispatchSerialQueue executor.
- UserMessageBubble's truncation probe typeset entire pasted-log-sized
  messages through CoreText per layout pass (100% main-thread pins in
  the 07-21 hang reports); certainly-long messages now skip the probe
  and render a prefix while collapsed.
- toolGroupSignature JSON-encoded every tool input in the transcript up
  to 12.5x/s on the MainActor; now a structural hash. The summary pass
  is trailing-throttled to 0.4s, and flatItems joins streaming chunks
  once instead of re-copying the prefix per delta.
- StatusFeedFetcher.parseDate allocated three formatters per call (86%
  of a pool thread in the 07-26 report); now shared statics.

Guest (vminitd) — teardown data loss and epoll registration hazards:
- IOPair no longer closes on a bare EPOLLHUP with a backpressure flush
  in flight (dropped the CLI's final output line); EPOLLOUT finishes the
  flush, then EOF closes loss-free. ManagedProcess.setExit closes only
  stdin, letting stdout/stderr self-close on EOF, with an 8s grace pass
  (patch #16).
- Epoll events carry a registration generation; the supervisor ignores
  stale events for recycled fd numbers. registerFd refuses EEXIST
  instead of clobbering the existing handler. TerminalIO's stdin relay
  writes a dup of the terminal fd so its backpressure registration
  can't collide with the stdout relay's (patch #17).
- VsockProxy flushes bytes parked toward the surviving peer on hangup,
  closes the dialing socket on a failed backend connect, and
  StandardIO/TerminalIO clean up partially-created pairs on setup
  failure (patch #16).

Full suite: 1451+292+74+20 tests, two failures — both pre-existing
environmental (MacVM base image absent on this machine; a load-flaky
liveness test that passes 3/3 in isolation).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-28 15:35:35 -07:00
abkslmandClaude Fable 5 9828a470eb Fix the recurring session-stall pair: MainActor lock-reconcile hang + vminitd relay spin
Host (dominant): AppStore.reconcileLocks polls every 3s on the MainActor
while any lock is held — effectively forever, since interrupted/errored
sessions deliberately retain locks. Each pass ran heldPathDisposition's
diverges() as three held×unmerged scans with two split-allocations per
pathsOverlap call, pinning the main thread for tens of seconds per pass
on a diverged trunk (hang-reports 2026-07-28: 100% of samples in
reconcileLocks→heldPathDisposition→pathsOverlap). That froze running
sessions' transcripts and starved the spawn path into the 60s
"produced no output" watchdog. pathsOverlap is now allocation-free
bytewise comparison with identical semantics, and divergentHeldPaths
answers all three questions from one O((held+unmerged)·depth) set.

Guest (persistence): VsockProxy threaded ONE offset pair through BOTH
relay directions; once the EAGAIN-return backpressure patch let pending
bytes persist, traffic in the other direction skewed the shared
counters, made the write leg unreachable, and spun the single
ProcessSupervisor poller thread forever — container-wide dead control
plane until VM recreation, triggered by exactly the backpressure the
host hang created. Each direction now owns its own pipe and counters
(OSFile.RelayDirection), and source EOF is only surfaced after the
pipe drains so SHUT_WR can't truncate a parked tail. Vendored patch
docs updated (#15); inert until the initfs image is rebuilt+repointed.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-28 15:05:37 -07:00
abkslm e37b9f8657 Merge nucleic/hazy-opal-yak-cjc0 into dev 2026-07-17 23:48:59 -07:00
abkslm f219491d54 Merge nucleic/upbeat-slate-lemur-7euo into dev 2026-07-17 22:21:12 -07:00
abkslm fd1b0e9bf7 Merge nucleic/quiet-opal-gecko-ah7w into dev 2026-07-17 16:03:36 -07:00
abkslm 1cd4d31ba7 Merge nucleic/tidy-north-gecko-6wqv into dev 2026-07-17 15:35:17 -07:00
abkslmandClaude Opus 4.8 f3c056fd0d docs: add CONTAINER_ISOLATION.md — session-isolation model + build/validate workflow
Overview doc so other agents/humans understand the shared-control-container
isolation work: the failure vectors + fixes (stdio wedge, connection leak,
control-plane HOL, OOM cross-kill, CPU/fork-bomb, per-session memory.max), the
host-vs-guest shipping surfaces, the per-exec cgroup layout + graceful fallback,
and the local vminit-image build/validate workflow + -nucleicN tag invariant.
Cross-linked from the vendored PATCHES.md.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 22:03:03 -07:00
abkslmandClaude Opus 4.8 67a2244173 Per-exec cgroups follow-up: host-configured hard memory.max (no protobuf)
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]>
2026-07-13 20:12:42 -07:00
abkslmandClaude Opus 4.8 df0e4c0029 Per-exec cgroups (guest patch #9): scope a session's OOM/CPU/fork-bomb to itself
Restructures the guest cgroup layout so each exec gets its OWN child cgroup
(/container/<id>/<execID>) with memory.oom.group=1, a fair cpu.weight, and a
pids.max backstop — so one control session can't OOM-kill, starve, or fork-bomb
its siblings in the shared container. The container init moves to its own leaf
so the container cgroup can delegate controllers to children (cgroup v2
no-internal-process rule). New Cgroup2Manager helpers: setOomGroup/setCpuWeight/
setPidsMax/remove.

Best-effort with graceful fallback: any failure in the per-exec setup wipes the
partial state and reverts to today's flat layout, and each exec falls back to the
container cgroup — a cgroup hiccup degrades to current behavior, never a failed
start.

COMPILE-VERIFIED via the musl cross-build; NOT yet runtime-validated. Built as
image tag -nucleic2; vminitReference stays on the validated -nucleic1 until
-nucleic2 is checked in a real container. A hard host-configured per-exec
memory.max (exec-RPC resources field) remains a follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 20:03:20 -07:00
abkslmandClaude Opus 4.8 3dd014405b vminit image: add 'make vminit-image-login' for Keychain-based GHCR auth
cctl login stores the credential in the macOS Keychain, which cctl images push
reads automatically — so pushing needs no REGISTRY_* env vars after a one-time
login. Env vars remain as a fallback.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 19:37:06 -07:00
abkslmandClaude Opus 4.8 b81d12553c Build the custom vminit image locally via make vminit-image; drop the CI workflow
The GitHub-hosted macos runners can't build the host framework (needs the
macOS 26+ Virtualization SDK), so publish the custom vminit guest image from a
local macOS 26/27 machine instead. Adds root-Makefile targets:
  - vminit-image-prep : one-time swiftly + musl static SDK install
  - vminit-image      : build cctl + cross-build vminitd, package the image
  - vminit-image-push : push to GHCR (REGISTRY_* env creds)
Forces WARNINGS_AS_ERRORS=false (Xcode Swift 6.4 rejects -warnings-as-errors
alongside SwiftPM's -suppress-warnings). Removes .github/workflows/vminit-image.yml
and repoints vminitReference + PATCHES.md docs at the Makefile.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-13 19:31:56 -07:00
abkslmandClaude Opus 4.8 3f939a84c6 containerization: guard patch #3's 'import os' behind #if canImport(os)
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]>
2026-07-13 19:15:11 -07:00
abkslmandClaude Opus 4.8 6fbc308fa4 Container isolation: fix stdio stall + per-exec connection leak; add guest image pipeline
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]>
2026-07-13 18:46:48 -07:00
NucleicandClaude Opus 4.8 3d02a0a2f7 Nucleic Control: spawn watchdog + reliable Stop + stdio diagnostics
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]>
2026-06-25 17:37:46 -07:00
NucleicandClaude Opus 4.8 f8b200128c Fix Nucleic Control container breakage: balloon, stop, interceptor
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]>
2026-06-25 13:55:31 -07:00
NucleicandClaude Opus 4.8 24fa2df429 Vendor apple/containerization with a VM-extensions forwarding patch
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]>
2026-06-21 20:22:21 -07:00