[Issue 1238] feat: retention policy for PTY session logs - #1243
Merged
Conversation
`~/.agent-farm/logs` had no cross-file bound: PtySession caps a single session's log at 50MB with one rotation, but nothing ever deleted the log of a session that ended. An audited machine reached 29,728 files / 19 GB, 97% of it untouched for over a month. Add session-log-sweep.ts: delete a session log once its mtime is older than AGENT_FARM_LOG_RETENTION_DAYS (default 30; 0 disables). mtime is the retention key because the last byte written is effectively when the session stopped producing output, and a dead session leaves no DB row to join against. Live sessions are excluded by id regardless of mtime — their log fd stays open, so unlinking under a live writer would redirect subsequent writes into an unlinked inode. Tower runs the sweep at startup (after reconcileTerminalSessions, so surviving sessions are in the manager's map and therefore excluded) and on an unref'd daily timer (AGENT_FARM_LOG_SWEEP_INTERVAL_MS). `codev doctor` reports the footprint as a health line, warning past 2 GB.
Addresses CMAP feedback on PR #1243. codex: doctor.ts gained user-visible logic with no focused tests, and read the real user-global ~/.agent-farm/logs inline. Extract checkSessionLogs(logDir, env) so the thresholds, retention-disabled wording and remediation hint are unit-testable against a temp dir with an injected env; add 8 tests using sparse files so the 2 GB threshold case costs no real disk. doctor() still passes the real log dir — that is the feature. Rendered output verified byte-identical after the refactor. claude: MS_PER_DAY was declared but unused. Export it and use it in tower-server.ts in place of an inline 24 * 60 * 60 * 1000.
…tor gitignore warning My original note blamed auditStateFileIgnore. The architect's pushback was correct — that function is fine. The real cause is findWorkspaceRoot() in doctor.ts matching packages/codev (the package directory) as a codev instance, so every workspaceRoot-derived check gets the wrong root when doctor runs from packages/. Reproduces on main. Records the repro and the node_modules/postSpawn details.
waleedkadous
added a commit
that referenced
this pull request
Jul 25, 2026
Alias session-log-sweep's formatBytes as formatLogBytes and update its call site; migration-backup-audit's formatBytes stays unaliased. Fixes the broken build on main introduced by PR #1243.
mohidmakhdoomi
pushed a commit
to mohidmakhdoomi/codev
that referenced
this pull request
Jul 25, 2026
…mith#1242/cluesmith#1243 semantic merge collision
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1238.
Summary
~/.agent-farm/logshad no cross-file bound.PtySessionalready caps a single session's log at 50 MB with one.log.1rotation, but nothing ever deleted the log of a session that ended — so every terminal Tower has ever opened left a file behind forever. The audited machine reached 29,728 files / 19 GB, 97% of it untouched for over a month.This adds the missing retention policy: a session log is deleted once its mtime is older than the retention window, enforced by Tower at startup and on a daily timer, with the footprint surfaced in
codev doctor.Key decisions
mtime is the retention key. The issue asks to "delete session logs N days after their session ends." The last byte written to a session log is effectively when that session stopped producing output — and a dead session's
terminal_sessionsrow is long since pruned, so there is nothing left to join against for a real end-timestamp. mtime gets the intended semantics with no new bookkeeping.Live sessions are excluded by id, not by mtime. A session's log fd stays open for the session's whole life. Unlinking under a live writer succeeds, but then silently redirects every subsequent write into an unlinked inode — losing exactly the log someone might be trying to read. A 30-day-idle live session is vanishingly unlikely, but the guard is one
Set.hasso it's in. This is also why the startup sweep is ordered afterreconcileTerminalSessions(): surviving sessions must be in the terminal manager's map before the sweep reads the active-id set.Env-configurable, following the husk-sweep precedent (
SHELLPER_HUSK_GRACE_MS) rather than.codev/config.json— the log directory is user-global (~/.agent-farm/logs), not per-workspace, so a per-workspace config key would be the wrong scope:AGENT_FARM_LOG_RETENTION_DAYS300disables the sweep entirely — an explicit opt-out for anyone treating these as an audit trail.AGENT_FARM_LOG_SWEEP_INTERVAL_MS86400000(24h)Both resolvers are NaN-checked rather than
parsed || default, because0is a meaningful override and is falsy in JS.Never throws. An unreadable log dir, a file that vanishes mid-scan, an unlink that fails — each is counted and skipped. Log retention is janitorial; it must not be able to take Tower's startup path down with it. This is a deliberate exception to the repo's fail-fast principle, scoped to the sweep only: a failed housekeeping pass should leave a log line, not a dead Tower.
Modelled on
shellper-husk-sweep.ts— same shape (pure functions + injectablenowseam, startup trigger + unref'd interval,clearIntervalingracefulShutdown) rather than a new pattern.What changed
agent-farm/servers/session-log-sweep.tssweepSessionLogs,measureSessionLogs, the two env resolvers,formatBytes. ~208 lines incl. rationale comments.agent-farm/servers/tower-server.tsgracefulShutdown.commands/doctor.tscodev/resources/arch.mdagent-farm/__tests__/session-log-sweep.test.ts__tests__/doctor-session-logs.test.tsTest plan
Unit — 18 new tests, using a real tmpdir with
utimesSync-forced mtimes and an injectednow(the module's whole job is filesystem behavior, so mockingfswould test the mock): retention boundary, bytes freed,.log.1rotations, live-session exclusion,retentionMs: 0no-op, non-log files and subdirectories ignored, missing directory, log-only-when-it-deleted, footprint measurement, all four resolver branches (default / override / explicit0/ garbage+negative), byte formatting.Full suite: 2205 passed, 0 failed (
src/__tests__/doctor.test.ts+ all ofsrc/agent-farm/__tests__/).porch checkgreen on bothbuildandtests.Verified against real data — not just green tests. I copied the live
~/.agent-farm/logs(737 files / 2.1 GB) preserving mtimes and ran the built module against the copy, never the real directory:Live-session exclusion on real aged data: passed a 23-day-old log's id as active against a 7-day retention — it survived while 429 others were deleted (
skippedActive: 1).codev doctorrender, from the built CLI:and the warning reaches the summary block with its remediation hint. (The real machine is over the 2 GB threshold, so this exercised the warn branch rather than the informational one.)
Not verified — flagging rather than papering over
I did not restart Tower to watch the startup sweep fire live. That would kill every running builder session in this workspace and needs your go-ahead. The wiring is typechecked and every function it calls is verified against real data, but the "Tower boots → sweep runs" edge itself is unexercised. Worth a look on the next Tower restart: the boot log should carry
Session logs: N file(s), X GB (retention 30d).Also worth knowing: this worktree spawned without
node_modules. My firstnpx tsc --noEmitexited 0 without resolving anything — a silent false pass. I caught it when vitest failed to load its own config, ranpnpm install --frozen-lockfile, and re-ran everything above. Every result in this PR is post-install.Unrelated observation (not fixed — out of scope)
codev doctorreports "Architect state files (codev/state/<name>.md) are not gitignored" in this worktree, but.gitignore:15-16does carrycodev/state/*.md+!codev/state/*_thread.md. Looks like a false positive inauditStateFileIgnore. Left alone — unrelated to #1238, but probably worth its own issue.CMAP consultation (codex + claude)
Skipped the agy/gemini lane deliberately — it's known-broken for
--typereviews (returns no VERDICT).claude — APPROVE (HIGH). One real finding:
MS_PER_DAYdeclared but never used. Accepted — exported it and used it intower-server.tsfor theretentionMscomputation, replacing an inline24 * 60 * 60 * 1000.codex — REQUEST_CHANGES (MEDIUM). Two findings:
"
doctor.tsgained user-visible logic but has no focused tests, and reads the real user-global~/.agent-farm/logs, which makes it insufficiently isolated." — Accepted. ExtractedcheckSessionLogs(logDir, env)so the thresholds, the retention-disabled wording and the remediation hint are unit-testable against a temp dir with an injected env. Addedsrc/__tests__/doctor-session-logs.test.ts— 8 tests, using sparse files (ftruncate) so the 2 GB threshold case costs no real disk: empty dir, below threshold, at threshold, one byte below (boundary inclusivity), configured retention, retention-disabled wording, missing dir, non-log files ignored. Confirmed the rendered output is byte-identical after the refactor. Note thatdoctor()itself still reads the real log dir — that is the feature, not a defect."Remove
codev/projects/1238-.../status.yamlandcodev/state/air-1238_thread.md— workflow/runtime artifacts, not feature code." — Rejected. These are Codev conventions the reviewer had no way to know:status.yamlis porch-managed and is committed to main at the pr-ready gate by design (it's where project completion stats live for non-bug projects), and the builder thread file's documented default disposition is COMMIT — it ships tomainundercodev/state/alongsidecodev/reviews/and becomes part of the historical review record. Stripping either would break the protocol rather than tidy the PR. Flagging rather than silently complying — happy to be overruled.Post-feedback: 26 tests across the two new files, full suite green.