feat: tatami gc — age-based reclamation across the global cache #1

Closed
opened 2026-07-10 03:10:21 +08:00 by weiwen · 0 comments
Owner

Summary

Add a tatami gc command that sweeps the global tatami cache (the user-level
root that holds every pool, same scope as prune --all) and reclaims idle,
inactive slots that have aged past a threshold. Dry-run by default; --yes to
execute; --delete-older-than <duration> sets the age cutoff.

Motivation

Neither existing reclamation command covers age-based cleanup of the whole cache:

  • prune only reclaims slots whose bound bookmark has landed on trunk
    (src/prune.rs classify). Anonymous or unlanded-but-abandoned work is never
    swept, so ~/.tatami accumulates slots (and whole pools) for repos and
    branches nobody has touched in months.
  • destroy is a deliberate, single-target (or single-pool) operation, not a
    cache-wide sweep.

gc fills the gap: "reclaim disk from everything I clearly abandoned," keyed on
age rather than merge status.

Semantics

A slot is a gc candidate when all of:

  1. Idle — no live owner (owner_alive) and no process cwd inside the slot
    (process::is_slot_in_use). Reuse the exact idle test prune/destroy share.
  2. Not leased — a lease is a durable reservation; skip it (mirror prune).
    Escalation via a flag could allow leased removal later, but default skips.
  3. Aged — last activity older than --delete-older-than.

Crucially, unlike prune, gc does not require the bookmark to be merged into
trunk. It is age-based and will remove unlanded work, so it is genuinely
destructive. That is why dry-run is the default and --yes is required to
execute (same contract as prune/destroy).

Flags

tatami gc [--delete-older-than <DURATION>] [--yes] [--verbose] [--json|--toon]
  • --delete-older-than <DURATION> — humantime-style duration (7d, 24h, 2w).
    Decide whether this is required or has a conservative default (e.g. no default =
    must be explicit, since the operation is destructive).
  • --yes — execute the removal instead of the dry run.
  • --verbose, -v — list each skipped slot with its reason (as prune -v does).
  • Honor the global --json / --toon structured-output contract; a gc result
    should aggregate per pool like PrunePool, reporting reclaimable/reclaimed
    bytes.

Open design questions

  1. What timestamp defines "age"? SlotEntry has created_at, leased_at,
    owner_started_at — but no last-used / last-returned timestamp. Keying gc
    on created_at would delete long-lived warm slots that are actively reused,
    which is wrong. Likely need a new last_used_at field stamped on acquire and
    on return, falling back to created_at (or the workspace dir mtime) for slots
    predating the field. This is the core prerequisite and should be settled first.
  2. Whole-pool reclamation. Should gc also remove a pool directory once its
    last slot is swept, and/or pools whose backing repo path no longer exists?
    The "global cache" framing points at reclaiming entire dead pools, not just
    slots.
  3. Leased slots. Skip by default (assumed). Offer an escalation flag
    (--include-leased) mirroring destroy, or leave leases strictly untouchable?
  4. Interaction with pre_destroy hooks. gc should run pre_destroy per slot
    like prune/destroy do.

Relationship to existing commands

  • prune = safe reclamation of landed work (merge-gated).
  • destroy = deliberate, targeted removal.
  • gc = cache-wide, age-gated reclamation of abandoned idle work, accepting
    that it may drop unlanded commits (hence dry-run default + --yes).

Implementation would live alongside src/prune.rs / src/destroy.rs and
src/cmd/, reusing prune_all's pool-discovery walk, the shared idle test, and
remove_slot.

## Summary Add a `tatami gc` command that sweeps the **global** tatami cache (the user-level root that holds every pool, same scope as `prune --all`) and reclaims idle, inactive slots that have aged past a threshold. Dry-run by default; `--yes` to execute; `--delete-older-than <duration>` sets the age cutoff. ## Motivation Neither existing reclamation command covers age-based cleanup of the whole cache: - `prune` only reclaims slots whose bound bookmark has **landed on trunk** (`src/prune.rs` `classify`). Anonymous or unlanded-but-abandoned work is never swept, so `~/.tatami` accumulates slots (and whole pools) for repos and branches nobody has touched in months. - `destroy` is a deliberate, single-target (or single-pool) operation, not a cache-wide sweep. `gc` fills the gap: "reclaim disk from everything I clearly abandoned," keyed on age rather than merge status. ## Semantics A slot is a **gc candidate** when all of: 1. **Idle** — no live owner (`owner_alive`) and no process cwd inside the slot (`process::is_slot_in_use`). Reuse the exact idle test `prune`/`destroy` share. 2. **Not leased** — a lease is a durable reservation; skip it (mirror `prune`). Escalation via a flag could allow leased removal later, but default skips. 3. **Aged** — last activity older than `--delete-older-than`. Crucially, unlike `prune`, gc does **not** require the bookmark to be merged into trunk. It is age-based and will remove unlanded work, so it is genuinely destructive. That is why dry-run is the default and `--yes` is required to execute (same contract as `prune`/`destroy`). ## Flags ``` tatami gc [--delete-older-than <DURATION>] [--yes] [--verbose] [--json|--toon] ``` - `--delete-older-than <DURATION>` — humantime-style duration (`7d`, `24h`, `2w`). Decide whether this is required or has a conservative default (e.g. no default = must be explicit, since the operation is destructive). - `--yes` — execute the removal instead of the dry run. - `--verbose`, `-v` — list each skipped slot with its reason (as `prune -v` does). - Honor the global `--json` / `--toon` structured-output contract; a gc result should aggregate per pool like `PrunePool`, reporting reclaimable/reclaimed bytes. ## Open design questions 1. **What timestamp defines "age"?** `SlotEntry` has `created_at`, `leased_at`, `owner_started_at` — but **no last-used / last-returned timestamp**. Keying gc on `created_at` would delete long-lived warm slots that are actively reused, which is wrong. Likely need a new `last_used_at` field stamped on acquire and on return, falling back to `created_at` (or the workspace dir mtime) for slots predating the field. This is the core prerequisite and should be settled first. 2. **Whole-pool reclamation.** Should gc also remove a pool directory once its last slot is swept, and/or pools whose backing repo path no longer exists? The "global cache" framing points at reclaiming entire dead pools, not just slots. 3. **Leased slots.** Skip by default (assumed). Offer an escalation flag (`--include-leased`) mirroring `destroy`, or leave leases strictly untouchable? 4. **Interaction with `pre_destroy` hooks.** gc should run `pre_destroy` per slot like `prune`/`destroy` do. ## Relationship to existing commands - `prune` = safe reclamation of **landed** work (merge-gated). - `destroy` = deliberate, targeted removal. - `gc` = cache-wide, **age-gated** reclamation of abandoned idle work, accepting that it may drop unlanded commits (hence dry-run default + `--yes`). Implementation would live alongside `src/prune.rs` / `src/destroy.rs` and `src/cmd/`, reusing `prune_all`'s pool-discovery walk, the shared idle test, and `remove_slot`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/tatami#1
No description provided.