Codev process fleet drives macOS memory-pressure kills: shellper husk sweep + fleet observability#1234
Conversation
…gle-snapshot husk preview
Architect Integration ReviewHigh-risk band (Tower core, ~950 production+test lines) — full production-diff review, with focused verification of the post-consult fix commit since PIR's single-pass consultation left it unreviewed. APPROVE. Scope compliance (architect-scoped at spawn): exact. Items 1+3 delivered — three sweep triggers (startup after reconciliation, hourly timer via The sweep predicate is safer than the issue demanded. Beyond the required unregistered∧childless∧aged conjunction: (1) The unreviewed fix commit (3a5083e) — verified directly, it's correct. codex's finding was real ( Field validation: the predicate this ships is the same one hand-verified on this machine yesterday — 19 husks identified by exactly these conditions, killed with zero collateral (2026-07-23 census on #1227). Architect integration review |
PIR Review: Shellper Husk Sweep + Fleet Memory Observability
Fixes #1227
Summary
macOS memory-pressure kills (jetsam) were traced to Codev's own process fleet: shellper wrapper processes whose PTY child already exited ("husks") were permanently unreapable, because the existing
killOrphanedShellpers()unconditionally protects any shellper whose socket still responds — and a husk's socket always responds. This PR adds a second, stricter sweep (unregistered AND childless AND aged past a grace period) on three triggers (Tower startup, an hourly in-process timer, and an on-demandafx tower sweep-husksCLI command with dry-run/--applysemantics), plus fleet-wide RSS and unregistered-shellper-count observability in/healthandafx status. Idle-architect hibernation (the issue's proposal item 2) was explicitly excluded per architect scoping and is flagged as a follow-up spec.Files Changed
packages/codev/src/agent-farm/servers/process-census.ts(+44 / -0) — newpackages/codev/src/agent-farm/servers/shellper-husk-sweep.ts(+173 / -0) — newpackages/codev/src/agent-farm/commands/tower-sweep-husks.ts(+92 / -0) — newpackages/codev/src/agent-farm/servers/tower-server.ts(+36 / -0)packages/codev/src/agent-farm/servers/tower-routes.ts(+137 / -1)packages/codev/src/terminal/session-manager.ts(+21 / -2)packages/core/src/tower-client.ts(+55 / -0)packages/codev/src/agent-farm/lib/tower-client.ts(+3 / -0)packages/codev/src/agent-farm/cli.ts(+16 / -0)packages/codev/src/agent-farm/commands/status.ts(+29 / -2)packages/codev/src/agent-farm/__tests__/process-census.test.ts(+79 / -0) — newpackages/codev/src/agent-farm/__tests__/shellper-husk-sweep.test.ts(+190 / -0) — newpackages/codev/src/agent-farm/__tests__/shellper-husk-sweep.e2e.test.ts(+177 / -0) — newpackages/codev/src/agent-farm/__tests__/tower-routes-husks.e2e.test.ts(+142 / -0) — newpackages/codev/src/agent-farm/__tests__/tower-sweep-husks.test.ts(+117 / -0) — newpackages/codev/src/agent-farm/__tests__/status-fleet-observability.test.ts(+132 / -0) — new16 files changed, 1438 insertions(+), 5 deletions(-).
Commits
ff25c080[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Plan draftce24667f[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Plan revised7c01cf97[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Add process-census + shellper-husk-sweep predicate6a622dbe[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Wire startup + hourly husk-sweep triggers into Towerf4a9ee56[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Add husk preview/sweep routes + fleet observability in /healthaf130187[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Extend TowerClient with husk preview/sweep + fleet health fields73895847[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Add afx tower sweep-husks CLI commandd2af62fc[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Surface fleet RSS + unregistered shellper count in afx status69bb42f3[PIR Codev process fleet drives macOS memory-pressure kills: stranded shellper husks accumulate per restart + no idle-fleet memory policy #1227] Correct stale give-up-reap comment re: husk sweep eligibilityTest Results
porch check(build + tests): ✓ both passtsc --noEmitclean acrosspackages/coreandpackages/codevprocess-census.test.ts,shellper-husk-sweep.test.ts,tower-sweep-husks.test.ts,status-fleet-observability.test.ts, plus the full existingsession-manager.test.tsregression suite (79 tests, unmodified behavior)ps/signals, no mocks):shellper-husk-sweep.e2e.test.ts— creates a genuine husk (deletes itsterminal_sessionsrow, kills its PTY child directly), confirms the periodic timer reaps it on the next tick; separately confirms graceful shutdown clears the new interval without hangingtower-routes-husks.e2e.test.ts— confirmsGET /api/shellpers/huskslists a genuine husk without touching it,/healthreports it underunregisteredShellperCount, andPOST /api/shellpers/husks/sweepthen actually reaps itshellper-cleanup.e2e.test.tsregression suite: unmodified, still passingafx tower sweep-husks(preview and--apply/-y),afx status(human +--json) reviewed at thedev-approvalgateArchitecture Updates
Added a "Husk sweep (Issue #1227)" paragraph to
codev/resources/arch.md's "Orphan Session Detection" section — the existing text there stated shellpers are "never killed" during the SQLite sweep, which is no longer the complete picture now that a second, stricter sweep exists for exactly the case that policy leaves permanently unreaped. Routed to the cold tier (arch.md), notarch-critical.md: this is subsystem-specific mechanism detail, not a cross-cutting fact that should change unrelated implementation decisions, and the hot tier's existing "Agent Farm Internals" map entry already points here.Lessons Learned Updates
Added one entry to
codev/resources/lessons-learned.mdunder "Debugging and Root Cause Analysis" (cold tier — the section already holds this exact class of JS falsy-coercion footgun, e.g. the[From 0107]body && body.nameentry):parsed || fallbackDefaultsilently discards a legitimate0override, since0is falsy in JS. I hit this for real —SHELLPER_HUSK_GRACE_MS=0(set by my own E2E test for an immediate-eligibility grace period) silently reverted to the 1-hour default, and only a real running-process assertion caught it, not a unit test mocking the parse. Fixed withNumber.isNaN(parsed) ? fallbackDefault : parsedfor both the grace-period and sweep-interval env vars (tower-server.ts,shellper-husk-sweep.ts'sresolveHuskGraceMs).Things to Look At During PR Review
killOrphanedShellpers. I deliberately did not modify the existing, well-testedkillOrphanedShellpers()in place — its "responsive socket protects" behavior is still correct and needed for the reconnectable case; the newsweepShellperHusks()is strictly additive and covers only the gap. Rationale is documented in the plan's Risks & Alternatives section.0 || defaultbug (see Lessons Learned above) — worth a close look atresolveHuskGraceMs()inshellper-husk-sweep.tsand the parallel fix intower-server.ts'shuskSweepIntervalMsparsing, since this class of bug is easy to reintroduce.3a5083e3:process-census.tsoriginally usedexecFileSync, called synchronously from the/healthHTTP handler — blocking Tower's entire event loop (all open terminals' WebSocket traffic) for the duration of everypscall. This is a previously-fixed anti-pattern in this exact codebase (lessons-learned.md:160), reintroduced by this PR. RewrotelistProcessCensus()to use non-blockingexecFile(matchingsession-manager.ts's existing async convention), added a regression test (process-census.test.ts:'never calls the synchronous execFileSync API') that fails against the pre-fix code. Full detail and a second, related fix (a redundant doublepsscan inhandleHuskPreview, also flagged independently by claude's own review) incodev/projects/1227-codev-process-fleet-drives-mac/1227-review-iter1-rebuttals.md. Per PIR's single-pass design this fix was not independently re-reviewed by either model — please verify it at this gate.afx tower sweep-husksUX mirrorsafx workspace recoverexactly (dry-run default,--apply,-y/--yes, sameconfirm()helper) per explicit architect direction mid-plan-review — not an independent design choice, so please check it actually matches that precedent rather than just looking reasonable in isolation.fleetRssKbscope: sums every in-scope shellper and its direct children (the full process-group tree), computed from onepsscan, regardless of DB-registration state — so a not-yet-swept husk still counts toward the visible number. This is explicitly advisory/observational, not used anywhere as a decision input (confirmed in conversation during the dev-approval gate review).computeRegisteredShellperPidsvalidates each DB row'sshellper_pidagainst the live process's actual start time (2000ms tolerance, same constantSessionManager.reconnectSessionalready uses) before counting it as registered — a stale row whose PID was reused by an unrelated process correctly falls through to "unregistered."How to Test Locally
afx dev pir-1227afx tower sweep-huskswith no flags previews only (nothing killed);--applyalone prompts for confirmation;--apply --yesreaps without promptingafx statusshowsFleet RSSandUnregistered Shellperslines when Tower is running;afx status --jsoncarries afleet: { rssKb, unregisteredShellperCount }field (explicitnulls, not omitted keys, when Tower is down)GET /healthon a running Tower includesfleetRssKbandunregisteredShellperCountSIGTERMexits promptly, no hang)Flaky Tests
None encountered.