Skip to content

feat: rework thread archive lifecycle with recovery and auto-delete - #436

Merged
SergeSerb2 merged 3 commits into
mainfrom
surgecode/archive-recovery-cleanup
Aug 2, 2026
Merged

feat: rework thread archive lifecycle with recovery and auto-delete#436
SergeSerb2 merged 3 commits into
mainfrom
surgecode/archive-recovery-cleanup

Conversation

@SergeSerb2

Copy link
Copy Markdown
Owner

Summary

Reworks what happens when a thread is archived, fixing three related problems:

  1. Archived threads kept their full worktree on disk. The tar.xz compression that was supposed to pack them often didn't happen (hundreds of archived threads = hundreds of full worktree copies). Archiving now retires the worktree: the server captures the complete working tree — including uncommitted and untracked work — into a per-thread checkpoint ref (refs/t3/checkpoints/<thread>/archive) in the repository, then removes the expanded worktree with git worktree remove. An archived thread now costs no disk beyond its branch and one checkpoint commit.
  2. Messaging an archived/dormant thread errored instead of recovering. If the worktree directory was gone (retired, or cleaned up by hand), the turn started against a missing directory and failed downstream. Now: sending a message to an archived thread emits thread.unarchived first (matching the existing unsettle/unsnooze-on-activity behavior), and both the unarchive reactor and the turn-execution boundary materialize the worktree — from the legacy tar.xz when one exists, otherwise by recreating it from the thread's persisted branch and overlaying the archived checkpoint so uncommitted work reappears as uncommitted work.
  3. Snoozed threads were auto-archived. An active (or malformed) snooze now suppresses auto-archive; an expired snooze falls through to the normal rules.

It also adds archive retention controls:

  • New ServerSettings.autoDeleteArchivedAfter (milliseconds or null; default six months = 183 days, null disables) and a new hourly auto-delete janitor that permanently deletes threads archived longer than the window, cascading through the normal thread.delete pipeline (session stop, terminal close, worktree removal, checkpoint-ref cleanup).
  • macOS Archive settings tab: new Auto-delete section (toggle + amount/unit editor sharing a new durationWindowRow helper with the Auto-archive row) and a Delete All button that pages through the whole archive and permanently deletes every archived thread after confirmation.

WorktreeArchiveStore loses its tar-creation path (superseded by branch-based retirement); restore/discardArchive remain so tarballs created before this change still restore. Thread deletion now also cleans up the archive checkpoint ref.

Area

  • apps/mac — native macOS app
  • apps/windows — Windows desktop app
  • apps/mobile — iPhone companion app
  • apps/server — backend server
  • Shared packages (packages/contracts, packages/shared, packages/client-runtime, …) or relay (infra/relay)
  • Build, CI, or release tooling
  • Docs

Release size

  • size:XS — tiny internal, documentation, or narrowly scoped fix
  • size:S — small user-visible fix or contained improvement
  • size:M — normal feature or meaningful behavior change
  • size:L — broad feature or substantial cross-package change
  • size:XL — release-system, architecture, or ecosystem-scale change

Verification

  • pnpm run verify --all passes (check + typecheck + tests; adds lint:mobile and the Swift suite) — 5/5 steps. Note: the Swift step flaked twice in SurgeCodeMacTests under load on earlier runs and passed on rerun with no changes; appears to be a pre-existing flake unrelated to this diff (the archive/settings Swift coverage added here passes consistently).
  • pnpm run test:mac passes (required for apps/mac changes) — 1158 tests across all three targets.
  • Manually verified (describe below)

New/updated automated coverage:

  • WorktreeLifecycle.test.ts (real-git): retire deletes the worktree and materialize recreates it from the branch with uncommitted + untracked state restored as uncommitted state; capture failure keeps the directory; missing branch/dir and unmanaged paths are safe no-ops.
  • WorktreeArchiveStore.test.ts: legacy tar.xz restore/discard and managed-path guards.
  • decider.archive.test.ts: thread.turn.start on an archived thread emits [thread.unarchived, thread.message-sent, thread.turn-start-requested].
  • AutoArchiveJanitor.test.ts: active/expired/malformed snooze eligibility. AutoDeleteJanitor.test.ts: window boundary, malformed archivedAt, null setting, per-thread error isolation.
  • Reactor tests: archive → retire / unarchive → materialize wiring, shared-worktree guard, deletion checkpoint-ref cleanup.
  • settings.test.ts / serverSettings.test.ts: 183-day default, explicit null survives, patch merge semantics.
  • AutoDeleteArchivedSettingsCodingTests.swift: wire coding of the new setting and patch (present/null/missing; explicit-null disable).
  • The git recovery semantics (restore --source overlay + clean + reset in a recreated worktree) were also verified end-to-end in a scratch repository.

Screenshots / Recordings

Not captured — headless agent environment without a running app instance. The UI reuses the existing Auto-archive settings row pattern (SettingsToggleRow + a shared amount/unit editor) and the existing destructive-action alert idiom.

Release notes

Archived threads now free their worktree disk space and recover automatically when messaged or unarchived; snoozed threads are no longer auto-archived; and archived threads older than six months are auto-deleted (configurable or disableable in Settings → Archive, alongside a new Delete All action).

…delete

Archived threads no longer keep their expanded worktree on disk. On
archive the server captures the full working tree — including
uncommitted and untracked work — into a per-thread checkpoint ref in
the repository, then removes the worktree, so an archived thread costs
no disk beyond its branch and one checkpoint commit. Unarchiving, or
simply sending a message to an archived thread, materializes it back:
from the legacy tar.xz when one exists, otherwise by recreating the
worktree from the thread's persisted branch and overlaying the
archived checkpoint so uncommitted work reappears as uncommitted work.

- thread.turn.start on an archived thread emits thread.unarchived
  (matching the unsettle/unsnooze-on-activity behavior), and the turn
  execution boundary materializes the worktree before the session
  starts — previously the turn proceeded against a missing directory
  and failed downstream instead of recovering
- snoozed threads are no longer auto-archived while the snooze is
  active (malformed or expired timestamps fall through safely)
- new auto-delete janitor permanently deletes threads archived longer
  than ServerSettings.autoDeleteArchivedAfter (default six months,
  null disables), cascading through the normal thread.delete pipeline
- WorktreeArchiveStore loses its tar-creation path (superseded by
  branch-based retirement in WorktreeLifecycle); restore/discard
  remain for archives created before the cutover
- thread deletion now also cleans up the archive checkpoint ref
The Archive settings tab gains an Auto-delete section (toggle plus
amount/unit editor, sharing a new durationWindowRow helper with the
Auto-archive row) bound to the server's autoDeleteArchivedAfter
setting, and a Delete All action that pages through the whole archived
list and permanently deletes every archived thread after confirmation,
tolerating per-thread failures.
@SergeSerb2 SergeSerb2 added the size:L Broad feature or substantial cross-package change label Aug 1, 2026
@SergeSerb2
SergeSerb2 merged commit afffa10 into main Aug 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L Broad feature or substantial cross-package change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant