Repository navigation
Conversation
Containment kills observed Codex app-server descendants by exact PID and treats a defunct process as terminated, but a zombie only clears when its parent consumes the exit status. After the tracked root exits, those killed descendants can linger as defunct children; in gateway-as-PID-1 deployments they are adopted and accumulate (openclaw#97616). Return the proven descendant identities from containment and retain an identity-scoped adopted-child reap after root exit, alongside the existing group-scoped reaper for the force-kill fallback path. The reap only waits zombies parented to this process, guards retained PIDs with their <bootId>:<startTicks> procfs identity, never waits the tracked root, and never waitpids unscoped PIDs. Exposed via openclaw/plugin-sdk/ process-runtime with docs. Related: openclaw#97616
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: blocked before merge. Reviewed October 8, 2026, 8:22 AM ET / 12:22 UTC (Revision 4). ClawSweeper reviewWhat this changesThe branch schedules scoped Linux zombie reaping after Codex app-server shutdown and exposes the cleanup schedulers through the Plugin SDK. Example: A Linux Gateway that adopts orphaned processes closes a Codex app-server.
Review scores
ProductKind: Bug fix · Worth it: Needs a maintainer decision · Fix scope: Partial Merge readiness⛔ Blocked before merge - 2 items remain This PR addresses a remaining Linux cleanup defect with sufficient real-process boundary proof and resolves the previous fixture-cleanup finding. No actionable patch defect was established; the public SDK additions still need an area-owner decision. Priority: P2 Decision needed
Before merge
FindingsNone. Agent review detailsHow this fits togetherOpenClaw owns the local Codex app-server transport and terminates its descendant processes when retiring a client. Linux additionally requires the adopting parent to consume exited descendants’ statuses before their process slots disappear. flowchart TD
A[Codex client retirement] --> B[Transport shutdown]
B --> C[Inspect and terminate descendants]
C --> D[Retain exact process identities]
D --> E[Tracked root exits]
E --> F[Check identity and adopted zombie state]
F --> G[Consume matching exit statuses]
Technical reviewBest possible solution: Keep cleanup with the existing transport and shared reaper owners through an explicitly accepted narrow SDK contract, while preserving the broader cross-owner umbrella. Do we have a high-confidence way to reproduce the issue? Yes: the recorded Linux subreaper regression exposes the failure through real transport close, and fetched-main source retains the same gap. This read-only review did not execute target code. Is this the best way to solve the issue? Yes for the repair boundary: retained-identity waiting complements existing containment without introducing a Gateway-wide reaper. The permanent SDK contract remains an owner choice. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against b33494d0dfdf. Provenance checked
TestingProof path: in-process harness. Added test files: 2. SecurityNone. EvidenceWhat I checked:
Likely related people:
PR surfaceSource +169, Tests +564, Docs +10. Total +743 across 7 files. View PR surface stats
Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. LabelsLabel changes: No label changes. Label justifications:
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (3 earlier review cycles)
|
…roups Replace the mocked scheduling assertions with a real-process regression. A subreaper harness spawns a fixture app-server root whose holders and relays span independent and shared process groups, closes it through the real transport boundary, and observes from /proc that every retained descendant is fully reaped while the tracked root completes normally and unrelated processes survive. A red control proves the observer detects an intentionally leaked adopted zombie, then reaps it through the exported scheduler. Related: openclaw#97616
|
Added the requested real behavior proof and pushed it as the regression itself. The new real-process test closes a fixture app-server tree (holders and relays across independent and shared process groups) through the real containment-to-transport boundary under a Linux subreaper harness, and observes /proc from outside: base |
…egression A failing run previously removed only the temporary files: the unrelated interval process was killed just on success and the fixture tree had no unconditional teardown, so failures could leave live processes or adopted zombies for later tests. Cleanup now terminates every roster PID and tracked handle, joins the tracked handles, and consumes any remaining harness-parented zombie statuses through the exported scheduler before the fixture files are removed. Related: openclaw#97616
|
Addressed the fixture-cleanup finding: teardown now runs unconditionally in both tests. It terminates every roster PID (the fixture log plus tracked handles), joins the tracked handles so libuv consumes their exits, then reaps any remaining harness-parented zombie statuses through the exported scheduler before the fixture files are removed. Verified on Debian: head run passes with zero leftover processes or zombies, and a forced failing run on the merge base kills every live fixture process (the base has no scheduler to consume the residual statuses, which die with the worker). Pushed as 1b538e7. The remaining items are the SDK-contract decisions for the area owner; per the review recommendation the two focused exports stay as documented. |
The extensions-test lane typechecks test files that the source-only extensions project skips. Replace the tuple cast on a slice with a standalone literal tuple and guard the two optional waitFor results the lane flagged. Related: openclaw#97616
The changed-extensions bundle-2 shard failed only a 120s crabbox-managed-binary acquisition timeout on a loaded 2-vCPU runner; the touched codex and process tests all passed. Related: openclaw#97616
Related: #97616
What Problem This Solves
Fixes part of the zombie-reaping umbrella: Codex app-server close kills observed descendants by exact PID and treats a defunct (
Z) process as terminated, but a zombie only clears when its parent consumes the exit status with waitpid. Descendants killed during close (hook relays, shells) can therefore linger as defunct children after the tracked root exits. In deployments where the gateway is PID 1 or the adoption point (fly.toml, Docker entrypoint overrides that drop tini), those zombies are adopted by the gateway and never waited, accumulating over hours into the hundreds and thousands reported in #97616 until the runtime degrades (spawn EAGAIN, uv_thread_create failures, connector silence).User Impact
User impact: long-running Linux gateways that repeatedly start and stop Codex app-servers now consume the exit statuses of the descendants their close path killed, so those defunct children stop accumulating. Deployments with a reaping PID 1 (tini, docker --init) are unaffected either way, and non-Linux platforms see no change.
Why This Change Was Made
terminateCodexAppServerDescendantsnow returns the exact descendant identities it proved and killed.scheduleAdoptedDescendantReapAfterRootExit, an identity-scoped sibling of the group-scoped reaper landed in fix(process): clean up adopted zombies after command timeout #139548. After the tracked root exits it reaps only retained PIDs that are zombies parented to the current process, guarded by their<bootId>:<startTicks>procfs identity against PID reuse. It never waits the tracked root (libuv owns that status), never waitpids unscoped PIDs, and installs no signal handlers, preserving the spawner-owned scoped-reaper design this umbrella established (the gateway-wide SIGCHLD approach from fix(process): add zombie reaper to prevent child process leak #97731 stays rejected).openclaw/plugin-sdk/process-runtimeso the extension reaches them through the supported boundary, with a docs paragraph in the SDK runtime guide. SDK direction: ClawSweeper recommended accepting these focused exports as narrow shared cleanup contracts; final public-API signoff belongs to the area owner.Evidence
Real behavior proof runs through the changed production boundary (containment to transport close to scheduled reaper) with real adopted descendants across process groups, and demonstrates base failure versus head success with the same test file.
Real-process regression:
extensions/codex/src/app-server/transport.close-reaping.process.test.ts. A Linux subreaper harness (mirroring gateway-as-PID-1 adoption) spawns a fixture app-server root whose holders and relays span independent and shared process groups, closes it through the realcloseCodexAppServerTransportAndWaitboundary, then observes /proc from outside the fixture tree. It asserts the close result is{ exited: true, cleanup: "closed" }(containment actually ran), the tracked root completed with exit code 0, every retained descendant fully left /proc, no run-owned zombie stays parented to the harness, and an unrelated process outside the tree keeps running. A red control first proves the observer detects an intentionally leaked adopted zombie that no close owns, then reaps it through the exported scheduler. Unconditional teardown terminates, joins, and reaps every task-owned fixture process on success and failure alike, so a failing run cannot leak processes or adopted zombies into later tests. This replaces the earlier mocked scheduling test per review feedback.Base failure (merge base
bd23988e, same test file overlaid), Debian Linux, real processes:The descendant killed by containment survives real transport close as a zombie parented to the harness (pid 1029), which is the reported leak shape. The control also fails on base because the exported scheduler does not exist there.
Head success (
8429c35c): the regression passes in GitHub CI Linux (changed-extensions bundle shard, both tests green) and passed 3 consecutive local Debian runs:Both tests pass: the contained close returns
closed, the root exits 0, and all four descendants (independent and shared groups) are fully gone from /proc, with the unrelated process alive. The red control confirms the green is not vacuous: the same observer does flag a stuck adopted zombie when one is planted, and forced failing runs on the merge base leave no live fixture processes behind.Unit coverage for the reaper itself stays in
src/process/scoped-child-reaper.test.ts(19 passed: reaps only identity-matching adopted zombies after root exit, skips PID reuse under a new start identity, never waits the tracked root, retains live descendants until they become adopted zombies, stops at its deadline, no work on darwin/win32, single registration per child, no scheduling without usable identities).Validation: CI fully green on
8429c35c(all 30 checks, including thecheck-test-typesextensions-test lane after satisfying its test typecheck, and the security/boundary scans). Locally:pnpm tsgo:core,pnpm tsgo:extensions,pnpm tsgo:extensions:test, targeted oxlint andoxfmt --check, and the three extension import-boundary checks pass. One earlier CI round hit an unrelatedcrabbox-managed-binary120 s acquisition timeout on a loaded 2-vCPU runner; the touched codex and process tests passed in that same round and the retry is green. This PR covers the containment-close owner; the umbrella stays open for other owners.