[#957] Lifecycle-aware cleanup of agent backend temp dirs#994
Conversation
Claude Code keeps temp under /tmp/claude-{uid} and never bounds it. On Linux
hosts where /tmp has a per-user quota, that dir grows until the quota is
exhausted, after which every Claude bash call fails silently (exit 1, no
output). QuadWork owns the agent lifecycle, so it cleans at known-safe moments.
- server/temp-cleanup.js: pure, never-throwing stale sweep. Deletion is
strictly age-based (max mtime/atime/ctime vs cutoff), because
/tmp/claude-{uid} is shared across all of a user's claude agents and
per-entry ownership isn't resolvable — so a live session's freshly-touched
files are always spared. Covers claude-{uid} entries + stray gemini crash
dumps; codex state lives under ~/.codex (no /tmp sweep). Config-gated via
temp_cleanup.{enabled,max_age_hours} (default enabled, 72h).
- server/index.js: sweep on agent teardown (deferred, stale-only) and on an
hourly timer + one at boot. Timer handle captured and cleared in shutdown()
per #972 clean-shutdown. Agent spawn/PTY path untouched.
- Tests: stale removed, live-session file preserved, gemini dumps, missing
dir / null uid no-op, never-throws, settings resolution.
- Docs: troubleshooting.md (silent-exit-1 post-mortem) + install-vps.md note.
Reimplemented fresh against current main (old PR #960 bitrotted after
#968/#972/#975 reworked index.js).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
#957 is folded under #967 as an isolated lifecycle fix; this adds bounded backend-temp cleanup without changing spawn/PTY behavior and clears the new timer in shutdown per #972.
Checked (evidence)
- Structural gate:
gh pr view 994 --json body→ filled## EPIC Alignmentand## Self-Verification; non-UI PR. - Issue/epic context:
gh issue view 957+gh api repos/realproject7/quadwork/issues/967→ lifecycle cleanup for/tmp/claude-{uid}, conservative age-based live-safety, periodic + teardown sweep, docs required. - Diff scope:
gh api repos/realproject7/quadwork/pulls/994/files→server/temp-cleanup.js,server/temp-cleanup.test.js,server/index.js,docs/troubleshooting.md,docs/install-vps.md. - Lifecycle wiring: teardown sweep at
server/index.js:793, boot/hourly timer atserver/index.js:2064, shutdown clear atserver/index.js:2317; no spawn/PTY hunks in the diff. - Cleanup behavior: stale predicate and Claude/Gemini sweep at
server/temp-cleanup.js:33andserver/temp-cleanup.js:67; settings default/opt-out atserver/temp-cleanup.js:116. - Tests: stale removed + fresh live-session file preserved at
server/temp-cleanup.test.js:36; gemini dump coverage atserver/temp-cleanup.test.js:57; null uid/bogus root/settings atserver/temp-cleanup.test.js:74. - Riskiest part of this diff: age-only cleanup inside shared
/tmp/claude-{uid}; acceptable because it keeps the shared dir itself, deletes only entries older than the conservative cutoff, exposes max age/disable config, and documents the ownership limitation. - Kill-list: scanned all items — clean.
- CI:
gh pr checks 994→test pass 52s.
Findings
- None.
Decision
Approving at 995d94ec98bbd0dea1d2c364fe80493f625ca53f; the change meets #957's lifecycle cleanup and documentation acceptance criteria with focused tests and clean shutdown integration.
|
@re2 APPROVE at Fresh reimplementation against current main (old #960 correctly treated as reference only). Scope is exactly the 5 files claimed: Checked (evidence)
No blocking issues. Standing no-merge rule — operator merges. |
Closes #957
Summary
Claude Code overrides
TMPDIRto/tmp/claude-{uid}and never bounds it. On Linux hosts where/tmpis mounted with a per-user quota (usrquota), that dir accumulates until the quota is exhausted — after which every Claudebashcall fails silently (exit 1, empty stdout/stderr) because Claude can't write its temp/sandbox-setup files before exec. Near-impossible to diagnose (masquerades as a bwrap/sandbox failure). QuadWork owns the agent lifecycle, so it's the right layer to keep backend temp bounded.server/temp-cleanup.js(new) — pure, never-throwing stale sweep. Deletion is strictly age-based (max(mtime, atime, ctime)vs a cutoff, likesystemd-tmpfiles):/tmp/claude-{uid}is shared by all of a user's claude agents and per-entry ownership isn't resolvable, so age is the safe signal — anything a live session still touches stays fresh and is spared. Coversclaude-{uid}entries + straygemini-client-error-*.jsoncrash dumps; codex keeps state under~/.codex(no/tmpsweep needed). Cross-platform: noprocess.getuid(Windows) → claude sweep skipped; missing dirs → no-op. Config-gated viatemp_cleanup.{enabled, max_age_hours}(default enabled, 72h).server/index.js— sweeps on agent teardown (stopAgentSession, deferred viasetImmediate, stale-only) and on an hourly timer + one at boot. The timer handle is captured in_tempSweepHandleand cleared inshutdown()per the [#S6] shutdown() cleanup + fix quadwork stop PID handling #972 clean-shutdown contract. A reentrancy guard prevents teardown/periodic overlap. Agent spawn path and PTY logic untouched.troubleshooting.md(full silent-exit-1 post-mortem + manual purge + config) and ainstall-vps.mdnote.server/temp-cleanup.test.js: stale removed / live-session file preserved / gemini dumps / missing-dir & null-uid no-op / never-throws / settings resolution.Reimplemented fresh against current main — the old PR #960 bitrotted after #968/#972/#975 reworked
index.js. PR #960's module design was used as reference; theindex.jsintegration targets current main (captured-handle +shutdown()clear, which the old PR lacked).EPIC Alignment
EPIC #967 — folded / isolated (per the batch brief). No sibling contracts to reconcile; this ticket stands alone. Change is confined to a new module + lifecycle wiring and does not alter EPIC-tracked surfaces.
Self-Verification
Adversarial diff re-read — production changes: new
temp-cleanup.js(pure), onerequire,backendTempSweepTick+ teardownsetImmediate, one captured-handle timer block, oneshutdown()clear line. No spawn/PTY code touched.Kill-list / safety invariants:
newestTimeMs(st) < cutoff; a live session's freshly-touched files test fresh and are kept (asserted:live-session-filepreserved,kept=1). Teardown sweep is stale-only and only reaches a session's own temp once that session is gone._tempSweepHandlecaptured and cleared inshutdown()alongside the other timers (the old PR [#957] Lifecycle-aware cleanup of agent backend temp dirs #960 used a baresetInterval— fixed here).stopAgentSession(teardown) gained a deferredsetImmediate; spawn/PTY untouched.sweepBackendTempwraps everything;backendTempSweepTicktry/catch/finally; verified against a bogustmpRoot.Build + test evidence:
npm test→ 63 passed, 0 failed, 2 skipped (pre-existing Jest-style skips).node server/pack-smoke.test.js→ PASS (24 shipped Node files;temp-cleanup.jsships underserver/).QUADWORK_SKIP_LISTEN=1 node -e "require('./server/index.js')"→ loads OK.Acceptance criteria (1:1):
~/.codex) ✅troubleshooting.md+install-vps.md) ✅Deviations
~/.codex, not/tmp, so there's nothing to sweep; documented in the module header rather than adding a dead code path./tmp/claude-{uid}is shared across all of a user's claude agents, per-entry live-ownership isn't resolvable; the conservative 72hmax(mtime,atime,ctime)window is the safe equivalent (a live session keeps touching its files, so they never age out). This matches the issue's own fallback: "conservative age + atime-based selection."