Skip to content

feat(storage): opt-in retention for archived tasks #5899

Description

@liugddx

Step 2 of the task lifecycle plan agreed in #5776. Step 1 has shipped: storage visibility (#5832), a durable archivedAt (#5884), and scoped manual cleanup with a Host-side age guard (#5896). Reclamation (#5855) is in progress separately.

This step adds one opt-in setting, delete archived tasks after N days. It is off by default. The Host decides and executes; Desktop exposes the setting and shows what happened. Automatic archiving (step 3) is out of scope.

Rules (from #5776)

  • Opt-in, per Host. Nothing happens until the user enables the setting on that Host.

  • Scope. It applies to every archived task, whether archived manually or automatically.

  • When the clock starts. A task's clock starts no earlier than the moment the policy was enabled: start = max(archivedAt ?? enabledAt, enabledAt). So enabling the setting never deletes a backlog at once, and tasks archived before feat(sessions): record when a task was archived #5884 (no archivedAt) count from enablement.

  • Restore and re-archive. Restoring a task cancels its deadline; archiving it again starts a fresh one.

  • Pinned tasks are never deleted. If any member of a task's revision family is pinned, the whole family is kept.

  • Recheck before delete. Right before deleting, the Host rechecks:

    • the policy;
    • the archive and pin state;
    • the elapsed time;
    • the existing retirement guards (running turns, queued messages, pending interactions, active Goals and resources, Agent Graph activity, scheduled tasks, WorkHub coordination).

    If any check fails, the task is skipped for this sweep.

Decisions (open to objection)

  1. Any settings change restarts the clock. Enabling the setting, or changing N, re-stamps enabledAt on the Host clock. Shortening N therefore never deletes anything immediately, and lengthening it only delays deletion. One rule, and it never surprises the user.

  2. Unattended sweeps are more conservative than manual delete. A sweep skips any family whose removal would reclaim a subagent worktree, or would archive still-active subtasks. Those are reported as "needs review" and stay visible for manual cleanup.

  3. Only rows the user can see. Candidates are the rows shown on Settings › Archived tasks: archived roots and orphaned archived subtasks. An archived subtask whose root still exists is not a candidate on its own.

  4. Clock. Wall time, with two guards:

    • the Host records the latest time it has observed, and eligibility never moves backwards;
    • if the wall clock is earlier than the newest timestamp the Host has recorded, the sweep pauses rather than act on a clock that went back.

    Work per tick is bounded, so catching up after a long time offline deletes in small batches. The residual risk is a system clock set far into the future, which deletes only what the user already opted into.

    A credited clock (uptime plus a fixed allowance) was considered and rejected: a local Host runs only while the app is open, so retention would barely progress for occasional users.

Proposed design

Setting. { enabled, days, enabledAt, revision } lives in its own small Host document in the State Root.

  • One writer. The only way to write it is a dedicated command, so agent settings tools and config import/export can never enable deletion.
  • Host-stamped time. enabledAt is stamped by the Host, never by the client.

Sweep. A new lane in HostStorageMaintenance. It runs only after Ready.

  • Cadence: idle every 15 min; when work remains, every 1 s, at most 8 families per tick.
  • Before the deadline: no SQL runs until enabledAt + days has passed.
  • Candidates: archived, not pinned, and archived_at IS NULL OR archived_at <= cutoff. The existing (is_flagged, is_archived, …) index bounds the scan, so no migration is needed.
  • Deletion: goes through the same Host-internal removal path as session.remove. The retention guard runs inside the removal admission, so a task that changes state mid-sweep is skipped, not deleted.

Results. Kept as latest-only state, with no lifetime counter:

  • the last sweep: time, deleted, skipped as busy, needs review, failed;
  • the last deletion: time, count, estimated bytes.

Logs carry counts only, never task names.

Protocol (epoch bump).

  • storage.retention.query returns the setting, a Host-computed preview ("N archived tasks become eligible on "), and the latest results.
  • storage.retention.set { expectedRevision, enabled, days }.

Mismatched epochs refuse to connect, so an older client never sits on a Host that deletes without showing it.

Desktop. A per-Host section on Settings › Archived tasks:

  • an enable switch;
  • days (30 / 60 / 90);
  • the Host preview;
  • "Last automatic cleanup: deleted N tasks (about X) on ", plus a "needs review" count.

The copy states that the setting applies to all archived tasks, that the clock starts when it's enabled, and that pinned tasks are kept.

PR plan

  1. End-to-end retention. One PR covering the setting document, the lane and guard, the two operations, and the Desktop section. Splitting it would ship either a switch that does nothing, or Host behaviour nobody can see or control. Every rule above is tested with an injected clock.
  2. Notification (optional). A one-time notice when a new automatic deletion has happened since the user last looked.

Question for maintainers

Storage location. I propose a dedicated Host document rather than a field on the runtime policy. The policy has several writers and travels with config export/import, so importing a config on another machine could carry this switch with it. Is a separate document acceptable, or should it live on the policy with explicit exclusions?

Refs #5776, #5825, #5884, #5896.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions