Skip to content

feat: automatic GC of stale team directories#8

Closed
RensTillmann wants to merge 5 commits into
tmustier:mainfrom
RensTillmann:feature/team-gc
Closed

feat: automatic GC of stale team directories#8
RensTillmann wants to merge 5 commits into
tmustier:mainfrom
RensTillmann:feature/team-gc

Conversation

@RensTillmann
Copy link
Copy Markdown

Problem

Every pi session creates a team directory under ~/.pi/agent/teams/. These are never cleaned up, accumulating indefinitely. On a moderately active system this quickly results in dozens of stale directories.

Solution

Three-layer cleanup:

  1. Startup GC — on session_start, silently scan and remove stale team directories. Fire-and-forget, never blocks the session.

  2. Exit cleanup — on session_shutdown, delete the current session's team directory if it's empty (no tasks, no teammates). Best-effort.

  3. /team gc command — manual bulk cleanup with --dry-run to preview and --force to skip confirmation.

What makes a team "dead" (safe to remove)?

All of the following must be true:

  • Not the current session's team
  • No active (non-stale) attach claim
  • No online workers
  • No in-progress tasks

New file: team-gc.ts

Reusable findGcCandidates() and gcTeamDirs() functions that leverage existing infrastructure (cleanupTeamDir(), loadTeamConfig(), assessAttachClaimFreshness(), listTasks()).

Changes

File What
extensions/teams/team-gc.ts Core GC logic (new)
extensions/teams/leader.ts Startup GC + exit cleanup
extensions/teams/leader-lifecycle-commands.ts /team gc command handler
extensions/teams/leader-team-command.ts Register gc subcommand + help text
skills/agent-teams/SKILL.md Documentation
README.md Documentation

Checks

  • tsc typecheck passes
  • eslint lint passes

Rens Tillmann and others added 5 commits March 6, 2026 05:38
Closes tmustier#4. Compatible with PR tmustier#6 (onDm callback).

Three notification layers:
1. Per-task: pi.sendMessage() injects completion info into leader
   context (triggerTurn: false) so it accumulates without interruption
2. Batch-complete: pi.sendUserMessage() wakes the idle leader when
   ALL tasks from a delegate() call finish
3. DMs (PR tmustier#6 compat): onDm callback with 50ms debounce batching,
   delivered via pi.sendMessage() with triggerTurn: true

DelegationTracker class tracks task ID batches from delegate calls.
Cleared on session_switch. Batches pruned after notification.
The original DelegationTracker.checkCompleted() polled listTasks() on
every inbox cycle, which raced — it could see stale task statuses and
fire false-positive batch-complete notifications immediately after
delegation.

Replaced with event-driven markCompleted(taskId): only marks a task
done when an idle_notification with completedTaskId is actually
received. No more filesystem polling for task status.

Also: batch completions are collected across all messages in one poll
cycle and deduplicated before firing notifications.
Add three-layer cleanup for team directories that accumulate across sessions:

1. **Startup GC** — on session_start, silently scan and remove stale team
   directories (no active attach claim, no online workers, no in-progress
   tasks). Fire-and-forget, never blocks the session.

2. **Exit cleanup** — on session_shutdown, delete the current session's team
   directory if it's empty (no tasks, no teammates). Best-effort.

3. **`/team gc` command** — manual bulk cleanup with `--dry-run` to preview
   and `--force` to skip confirmation. Same logic as startup GC but
   interactive.

A team is considered dead (safe to remove) when ALL of:
- Not the current session's team
- No active (non-stale) attach claim
- No online workers
- No in-progress tasks

New file: `team-gc.ts` with reusable `findGcCandidates()` and `gcTeamDirs()`
functions that leverage existing `cleanupTeamDir()`, `listDiscoveredTeams()`,
and `assessAttachClaimFreshness()` infrastructure.
tmustier pushed a commit that referenced this pull request Mar 22, 2026
Two-layer automatic cleanup for team directories that accumulate across sessions:

1. **Startup GC** — on session_start, fire-and-forget call to the existing
   gcStaleTeamDirs() with a 24h age floor. Reuses the well-tested function
   that checks age + online workers + in-progress tasks + attach claim
   freshness. Never blocks startup.

2. **Exit cleanup** — on session_shutdown, delete the current session's team
   directory if it's empty (no tasks in any namespace, no teammates were
   active, no fresh attach claim from another session). Best-effort, never
   blocks shutdown. Only cleans the session's own team (not attached teams).

Based on #8 by @RensTillmann.
tmustier added a commit that referenced this pull request Mar 22, 2026
)

Incorporates the safe parts of PR #8 (@RensTillmann).

**Startup GC:** fire-and-forget gcStaleTeamDirs() on session_start with 24h age floor,
excluding the current session's team.

**Exit cleanup:** delete own empty team dir on shutdown — checks all task namespaces,
attach claims, config workers, and RPC teammates before deleting.

Co-authored-by: Rens Tillmann <rens@inb0x.ai>
@tmustier
Copy link
Copy Markdown
Owner

The safe parts of this PR (startup GC + exit cleanup) have been incorporated into #30 with author attribution. The state-based findGcCandidates was not included — the existing age-based gcStaleTeamDirs() provides better safety guardrails. Thank you @RensTillmann! 🎉

@tmustier tmustier closed this Mar 22, 2026
@tmustier
Copy link
Copy Markdown
Owner

Hey @RensTillmann — cheers for this too, and sorry it took so long! The safe parts are implemented with credit in #30 (you're commit author + co-author on the squash).

The new behaviour is:

  • Startup GC: on session start, fire-and-forget cleanup of stale team dirs older than 24h — reuses the existing gcStaleTeamDirs() which checks age + online workers + in-progress tasks + attach claims. Current session's team is excluded
  • Exit cleanup: on session shutdown, deletes the session's own team dir if it's truly empty — checks all task namespaces (not just the active one), attach claims, config workers (manual/tmux), and RPC teammates before deleting
  • Added excludeTeamIds param to gcStaleTeamDirs() for the startup GC exclusion

We didn't take the state-only findGcCandidates / team-gc.ts module — the existing age-based gcStaleTeamDirs() already had the right safety guardrails and we didn't want to risk deleting teams with pending tasks. The /team gc --max-age-hours command is unchanged.

Shipped in v0.5.1

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.

2 participants