Skip to content

feat: Time Travel shadow-repo retention (orphan cleanup, optional age-out, stale-lock repair)#1775

Open
King0James0 wants to merge 1 commit into
agent0ai:mainfrom
King0James0:feat/time-travel-retention
Open

feat: Time Travel shadow-repo retention (orphan cleanup, optional age-out, stale-lock repair)#1775
King0James0 wants to merge 1 commit into
agent0ai:mainfrom
King0James0:feat/time-travel-retention

Conversation

@King0James0

Copy link
Copy Markdown

Problem

The _time_travel plugin keeps a hidden git repository per workspace under /a0/usr/.time_travel/workspaces/<id>/repo.git and snapshots it on every file change — but ships no retention of any kind:

  • Nothing deletes a shadow repository when its owner goes away. chat_remove never touches .time_travel, and there is no delete/prune endpoint in the plugin's API or web UI. Every removed chat/project leaves its full history behind forever, unreachable from the UI.
  • A stale index.lock permanently wedges snapshots. Any workspace whose git add ever exceeds GIT_TIMEOUT_SECONDS (large/busy work-trees do this reliably) strands repo.git/index.lock, and every later snapshot fails with index.lock: File exists until someone deletes it by hand.

Observed on a live instance: 518 shadow repositories / 12 GB, most belonging to long-deleted chats, plus one workspace whose snapshots had been failing for days on a stranded lock.

Fix

A throttled retention sweep inside the plugin, driven from job_loop (worker thread, at most one in flight, default every 6h):

  • Orphans — live workspace paths (project folders, the configured workdir, per-chat workdirs) are forward-enumerated and hashed with the existing workspace_id_for derivation; a shadow directory matching none of them has no owner and is removed after a grace window (default 24h).
  • Optional age-outretention_max_age_days (default 0 = keep forever) removes a live workspace's history when it has had no snapshot in N days. Always safe for the feature: Time Travel lazily re-initializes an empty history on the next snapshot.
  • Stale locksrepo.git/index.lock older than retention_stale_lock_minutes (default 30; git subprocesses are killed at GIT_TIMEOUT_SECONDS, so no legitimate lock lives that long) is removed, un-wedging future snapshots.
  • Corrupt-repo set-asidesrepo.git.invalid* backups past the grace window.

Deletion is refused for any path outside the shadow root.

Settings

Standard plugin-config surface (default_config.yaml + webui/config.html, settings_sections: [agent]): enable toggle, sweep interval, age-out days (0 = keep forever), orphan grace, stale-lock age. Defaults are conservative — only unreachable garbage is removed unless the operator opts into age-out.

Observability

Durable state next to the workspaces dir: retention.json (running totals + last sweep stamp) and retention.log (one JSON line per sweep naming exactly what was removed, tail-capped at 1000 lines).

Testing

  • tests/test_time_travel_retention.py: config defaults/clamps, full sweep matrix (orphan past/inside grace, aged, keep-forever default, stale vs fresh lock, invalid backup), disabled no-op, shadow-root deletion guard, marker accrual, per-sweep history + cap, throttle, workspace_id_for parity — 9/9.
  • End-to-end in a fresh agent0ai/agent-zero:latest container: boot clean, the job_loop sweep fired live (orphan removed, young orphan + live workspace kept, stale lock cleared), marker + history written, retention_enabled: false via saved plugin config correctly disables it, settings page renders.

…-out, stale-lock repair)

Time Travel keeps a hidden git repository per workspace under
/a0/usr/.time_travel/workspaces/<id>/repo.git and snapshots on every file
change, but ships no retention: nothing deletes a shadow repository when its
chat or project is removed (chat_remove never touches .time_travel), there is
no delete/prune endpoint in the API or web UI, and a workspace whose 'git add'
ever exceeds GIT_TIMEOUT_SECONDS strands repo.git/index.lock, failing every
later snapshot with 'index.lock: File exists'. Observed on a live instance:
518 shadow repositories / 12 GB, most belonging to long-deleted chats, plus a
permanently wedged workspace.

This adds a throttled retention sweep driven from job_loop:

- Orphans: live workspace paths (project folders, the configured workdir,
  per-chat workdirs) are forward-enumerated and hashed with the existing
  workspace_id_for derivation; a shadow directory matching none of them is
  unreachable from the UI forever and is removed after a grace window.
- Optional age-out: retention_max_age_days (default 0 = keep forever) removes
  a live workspace's history when it has had no snapshot in N days. Time
  Travel lazily re-initializes an empty history on the next snapshot, so this
  is always safe for the feature.
- Stale locks: repo.git/index.lock older than retention_stale_lock_minutes is
  removed (git subprocesses are killed at GIT_TIMEOUT_SECONDS, so no
  legitimate lock lives that long), un-wedging future snapshots.
- Corrupt-repo set-asides (repo.git.invalid*) past the grace window.

Deletion is refused for any path outside the shadow root. Settings render in
the existing plugin-config pattern (default_config.yaml + webui/config.html,
settings_sections: agent): enable toggle, sweep interval, age-out days,
orphan grace, stale-lock age. Durable evidence lives next to the workspaces
dir: retention.json (running totals + last sweep stamp) and retention.log
(one JSON line per sweep naming everything removed, tail-capped at 1000).

Sweeps run in a worker thread off the event loop, at most one in flight,
default every 6 hours.

Tests: tests/test_time_travel_retention.py (config defaults/clamps, the full
sweep matrix, keep-forever default, disabled no-op, deletion guard, marker
accrual, per-sweep history + cap, throttle, and workspace_id_for parity).
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.

1 participant