agent-zero/plugins/_time_travel/webui/config.html
King0James0 c42dffa54e feat: Time Travel shadow-repo retention (orphan cleanup, optional age-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).
2026-08-12 14:34:16 +02:00

104 lines
5.3 KiB
HTML

<html>
<head>
<title>Time Travel</title>
</head>
<body>
<div x-data>
<template x-if="config">
<div>
<div class="section-title">Time Travel retention</div>
<div class="section-description">
Time Travel keeps a hidden history of every workspace it snapshots. Retention
keeps that storage bounded by cleaning up histories nothing can reach anymore
(deleted chats and projects) and, optionally, aging out old history.
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Enable retention</div>
<div class="field-description">
Periodically clean up orphaned workspace histories, stale snapshot
locks, and corrupt-repository backups. Turning this off means Time
Travel storage grows without bound.
</div>
</div>
<div class="field-control">
<label class="toggle">
<input type="checkbox" x-model="config.retention_enabled"
x-init="if (config.retention_enabled === undefined || config.retention_enabled === null) config.retention_enabled = true" />
<span class="toggler"></span>
</label>
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Sweep interval (hours)</div>
<div class="field-description">
How often the cleanup runs.
</div>
</div>
<div class="field-control">
<input type="number" min="1" step="1"
x-init="if (config.retention_sweep_interval_hours === undefined || config.retention_sweep_interval_hours === null) config.retention_sweep_interval_hours = 6"
x-model.number="config.retention_sweep_interval_hours" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Delete history older than (days)</div>
<div class="field-description">
Leave at 0 (the default) and workspace history is kept forever. Set a
number — say 30 — and a workspace with no snapshot in that many days
has its history deleted. Time Travel simply starts a fresh history on
the workspace's next change.
</div>
</div>
<div class="field-control">
<input type="number" min="0" step="1"
x-init="if (config.retention_max_age_days === undefined || config.retention_max_age_days === null) config.retention_max_age_days = 0"
x-model.number="config.retention_max_age_days" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Orphan grace period (hours)</div>
<div class="field-description">
History belonging to a deleted chat or project is unreachable from the
UI and gets cleaned up — but only after it has been inactive this long,
so recent work is never raced.
</div>
</div>
<div class="field-control">
<input type="number" min="1" step="1"
x-init="if (config.retention_orphan_grace_hours === undefined || config.retention_orphan_grace_hours === null) config.retention_orphan_grace_hours = 24"
x-model.number="config.retention_orphan_grace_hours" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Stale snapshot lock age (minutes)</div>
<div class="field-description">
A leftover git index.lock older than this is removed so snapshots stop
failing with "index.lock: File exists". Time Travel's own git
operations are killed after 20 seconds, so no legitimate lock lives
this long.
</div>
</div>
<div class="field-control">
<input type="number" min="5" step="1"
x-init="if (config.retention_stale_lock_minutes === undefined || config.retention_stale_lock_minutes === null) config.retention_stale_lock_minutes = 30"
x-model.number="config.retention_stale_lock_minutes" />
</div>
</div>
</div>
</template>
</div>
</body>
</html>