Skip to content

[Issue 1238] feat: retention policy for PTY session logs - #1243

Merged
waleedkadous merged 8 commits into
mainfrom
builder/air-1238
Jul 25, 2026
Merged

[Issue 1238] feat: retention policy for PTY session logs#1243
waleedkadous merged 8 commits into
mainfrom
builder/air-1238

Conversation

@waleedkadous

@waleedkadous waleedkadous commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #1238.

Summary

~/.agent-farm/logs had no cross-file bound. PtySession already caps a single session's log at 50 MB with one .log.1 rotation, 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_sessions row 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.has so it's in. This is also why the startup sweep is ordered after reconcileTerminalSessions(): 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:

Variable Default Meaning
AGENT_FARM_LOG_RETENTION_DAYS 30 Age at which a log is deleted. 0 disables the sweep entirely — an explicit opt-out for anyone treating these as an audit trail.
AGENT_FARM_LOG_SWEEP_INTERVAL_MS 86400000 (24h) Sweep cadence. Floored at 1s.

Both resolvers are NaN-checked rather than parsed || default, because 0 is 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 + injectable now seam, startup trigger + unref'd interval, clearInterval in gracefulShutdown) rather than a new pattern.

What changed

File Change
agent-farm/servers/session-log-sweep.ts New. sweepSessionLogs, measureSessionLogs, the two env resolvers, formatBytes. ~208 lines incl. rationale comments.
agent-farm/servers/tower-server.ts Startup sweep + footprint log line + unref'd daily timer; interval cleared in gracefulShutdown.
commands/doctor.ts New "Session Logs" section: file count + footprint + active retention, warning past 2 GB with a remediation hint.
codev/resources/arch.md Paragraph documenting the policy, next to the existing husk-sweep one.
agent-farm/__tests__/session-log-sweep.test.ts New. 18 unit tests for the sweep.
__tests__/doctor-session-logs.test.ts New. 8 unit tests for the doctor check (added after CMAP — see below).

Test plan

Unit — 18 new tests, using a real tmpdir with utimesSync-forced mtimes and an injected now (the module's whole job is filesystem behavior, so mocking fs would test the mock): retention boundary, bytes freed, .log.1 rotations, live-session exclusion, retentionMs: 0 no-op, non-log files and subdirectories ignored, missing directory, log-only-when-it-deleted, footprint measurement, all four resolver branches (default / override / explicit 0 / garbage+negative), byte formatting.

Full suite: 2205 passed, 0 failed (src/__tests__/doctor.test.ts + all of src/agent-farm/__tests__/). porch check green on both build and tests.

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:

BEFORE: 737 files, 2.1 GB, oldest=2026-06-24T08:31:12.921Z
30d retention → deleted 12 log(s), freed 29 MB
 7d retention → deleted 418 log(s), freed 1.4 GB

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 doctor render, from the built CLI:

Session Logs (~/.agent-farm/logs)

  ⚠ 741 file(s), 2.1 GB (retention 30d)

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 first npx tsc --noEmit exited 0 without resolving anything — a silent false pass. I caught it when vitest failed to load its own config, ran pnpm install --frozen-lockfile, and re-ran everything above. Every result in this PR is post-install.

Unrelated observation (not fixed — out of scope)

codev doctor reports "Architect state files (codev/state/<name>.md) are not gitignored" in this worktree, but .gitignore:15-16 does carry codev/state/*.md + !codev/state/*_thread.md. Looks like a false positive in auditStateFileIgnore. 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 --type reviews (returns no VERDICT).

claude — APPROVE (HIGH). One real finding: MS_PER_DAY declared but never used. Accepted — exported it and used it in tower-server.ts for the retentionMs computation, replacing an inline 24 * 60 * 60 * 1000.

codex — REQUEST_CHANGES (MEDIUM). Two findings:

  1. "doctor.ts gained user-visible logic but has no focused tests, and reads the real user-global ~/.agent-farm/logs, which makes it insufficiently isolated."Accepted. Extracted checkSessionLogs(logDir, env) so the thresholds, the retention-disabled wording and the remediation hint are unit-testable against a temp dir with an injected env. Added src/__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 that doctor() itself still reads the real log dir — that is the feature, not a defect.

  2. "Remove codev/projects/1238-.../status.yaml and codev/state/air-1238_thread.md — workflow/runtime artifacts, not feature code."Rejected. These are Codev conventions the reviewer had no way to know: status.yaml is 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 to main under codev/state/ alongside codev/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.

`~/.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
waleedkadous merged commit c8d8be7 into main Jul 25, 2026
6 checks passed
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.
waleedkadous added a commit that referenced this pull request Jul 25, 2026
…lision

hotfix(doctor): resolve formatBytes duplicate-identifier collision from #1242 × #1243
mohidmakhdoomi pushed a commit to mohidmakhdoomi/codev that referenced this pull request Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agent-farm: PTY session logs have no rotation or retention policy

1 participant