Skip to content

feat(tasks): auto-archive or delete tasks whose PR has been merged#2688

Open
SpielerNogard wants to merge 19 commits into
generalaction:mainfrom
SpielerNogard:emdash/automatic-archive-of-merged-tasks-hg9f2
Open

feat(tasks): auto-archive or delete tasks whose PR has been merged#2688
SpielerNogard wants to merge 19 commits into
generalaction:mainfrom
SpielerNogard:emdash/automatic-archive-of-merged-tasks-hg9f2

Conversation

@SpielerNogard

@SpielerNogard SpielerNogard commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Description

Adds an opt-in global setting under Settings → General → Auto-cleanup merged tasks that periodically archives or deletes tasks whose linked pull request has been merged, after the task has been idle for a user-configurable delay.

  • Configurable action: Archive (default) or Delete (with an optional "also delete the branch" checkbox).
  • Configurable delay: number input + unit dropdown (Minutes / Hours / Days), default 24h.
  • Runs on the same 5-minute cadence as the existing PR sync scheduler.
  • Manually restoring an archived task bumps its lastInteractedAt, so the user's restore counts as fresh activity and the task is not re-archived until at least the configured delay has passed.
  • Off by default.

How it works. A new singleton AutoCleanupScheduler ticks every 5 minutes. Each tick reads the tasks settings (no-op if disabled), then selects candidates where tasks.archivedAt IS NULL AND the linked PR is merged AND the task's last activity (COALESCE(lastInteractedAt, updatedAt)) is older than the configured delay. PR↔task matching is scoped via project_remotes to avoid cross-repo branch-name collisions. Each candidate is run through the existing archiveTask / deleteTask operations. Per-task error isolation and a re-entrancy guard so overlapping ticks no-op.

The "time since last task activity" trigger means an actively-worked-on task isn't archived just because its PR happened to merge a while ago, and gives the user a natural way to keep a restored task around — simply interact with it before the delay expires.

Touched areas:

  • New main-process scheduler: apps/emdash-desktop/src/main/core/tasks/auto-cleanup-scheduler.ts + unit tests.
  • New settings keys under tasks (zod-validated, defaults wired): autoCleanupMergedEnabled, autoCleanupMergedAction, autoCleanupMergedDeleteBranch, autoCleanupMergedDelayMs.
  • restoreTask bumps lastInteractedAt so restored tasks are not immediately re-archived on the next tick.
  • New telemetry event task_auto_cleaned_up.
  • New renderer duration helper (durationToMs / msToDuration) for the settings UI.
  • New settings row component AutoCleanupMergedTasksRow in the General settings tab, with a 300 ms debounced number input.
  • Scheduler wired into the app lifecycle (main/index.ts initialize, main/app/shutdown.ts dispose).

Related issues

None.

Testing

  • pnpm run format — clean
  • pnpm run lint — clean (no new warnings)
  • pnpm run typecheck — clean
  • pnpm run test — 2081/2081 unit tests pass (2 Playwright browser test files fail at runner setup due to local Playwright install; unrelated to this change)
  • pnpm --dir apps/emdash-desktop run test:migrations — clean
  • Manual: toggle on with delay 1 hour against a production-DB snapshot, confirmed a known-merged task is archived on the next tick (< 5 min); manually unarchived the task and confirmed it is not re-archived on subsequent ticks (because lastInteractedAt was just bumped).

Screenshot/Recording (if applicable)

Before cluttered workspaces
Bildschirmfoto 2026-06-26 um 07 08 05

New setting in general settings
Bildschirmfoto 2026-06-26 um 07 03 49

Enough space for new ideas :)
Bildschirmfoto 2026-06-26 um 07 53 13

Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks or explained why not
  • I updated docs when behavior or setup changed — no docs needed; behavior is self-evident in Settings UI
  • I added or updated tests when behavior changed, or explained why not
  • I only added comments where the logic is not obvious
  • I used Conventional Commits for commit messages and, when possible, the PR title

…ncy guard and input debounce

- Scope candidate PRs to the task project's known remotes (project_remotes)
  to prevent cross-repo branch-name collisions from triggering archive/delete.
- Guard runOnce() with a _running flag so overlapping ticks no-op.
- Replace raw sql`IS NULL` with isNull(tasks.archivedAt) for consistency.
- Debounce the settings duration <Input> by 300ms with commit-on-blur,
  matching the design spec.
When a repository has any merged PR with merged_at IS NULL (legacy rows
from before the column was added), reset its full/incremental sync cursors
on the next sync() call so a fresh full sync re-pulls and populates the
field. Cached per process so the cheap DB probe runs at most once per repo
per app lifetime. Defensive try/catch ensures a probe failure never breaks
the sync path.
…erged_at

Replaces the 'time since PR merge' trigger with 'time since last task
interaction' so an actively-worked-on task isn't archived just because
its PR happened to merge a while ago. lastInteractedAt is the same
timestamp the sidebar already displays, falling back to updatedAt when
the task has never been interacted with.

- Scheduler joins on tasks.lastInteractedAt (COALESCEd with updatedAt)
  instead of pullRequests.merged_at; status='merged' still required.
- Removes the pullRequests.merged_at column (migration 0017) and all
  associated GitHub-sync, GraphQL fragment, and backfill code.
- Migration 0016 still ships (adds auto_cleanup_opt_out); 0017 cleans up
  merged_at before the feature reaches users.
Without this log it was impossible to tell from the log file whether the
scheduler was running and how many tasks it considered each tick.
@SpielerNogard SpielerNogard force-pushed the emdash/automatic-archive-of-merged-tasks-hg9f2 branch from 8babd9e to c540ab2 Compare June 26, 2026 06:14
…restore

When the user manually restores an archived task, treat the restore as
fresh activity by bumping lastInteractedAt. This subsumes the opt-out
flag: the scheduler already excludes tasks whose lastInteractedAt is
within the configured delay. A user who intentionally keeps a restored
task around will interact with it again before the delay expires; one
who restores and forgets has effectively asked for re-cleanup.

- Removes tasks.auto_cleanup_opt_out column and the two unmerged
  migrations (0016 add, 0017 drop merged_at) that the previous design
  required. Branch now ships with zero schema changes.
- restoreTask bumps lastInteractedAt instead of setting opt-out.
- Scheduler drops the opt-out filter.
- Updated tests and legacy-port fixture schemas.
db:fixtures regenerates these files non-deterministically (SQLite page
allocation, hidden timestamps) even when the schema is unchanged. The
regenerated bytes pass the migration tests just as upstream's bytes do,
so keep upstream's bytes to avoid noise in the diff.
@SpielerNogard SpielerNogard marked this pull request as ready for review June 26, 2026 06:30
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds automatic cleanup for tasks whose linked pull request has merged. The main changes are:

  • New main-process scheduler for archive/delete cleanup.
  • New task settings, defaults, validation, and telemetry event.
  • Restore behavior that refreshes task activity.
  • New General settings UI for action, branch deletion, and delay.

Confidence Score: 4/5

The cleanup path can act on unintended task rows and the delay UI can save a shorter delay than the user entered.

  • The scheduler starts before settings initialization finishes.
  • The candidate query does not filter out non-user task types.
  • The unit selector can discard the typed draft and persist the wrong delay.
  • Telemetry loses the configured delay field.

auto-cleanup-scheduler.ts, index.ts, TaskSettingsRows.tsx, telemetry sanitizer

Important Files Changed

Filename Overview
apps/emdash-desktop/src/main/core/tasks/auto-cleanup-scheduler.ts Adds the cleanup loop, candidate query, and archive/delete dispatch; task scoping and delete options need attention.
apps/emdash-desktop/src/main/index.ts Starts the scheduler during app boot before settings initialization has completed.
apps/emdash-desktop/src/renderer/features/settings/components/TaskSettingsRows.tsx Adds the settings row; the delay input can persist the wrong value when the unit changes before debounce completes.
apps/emdash-desktop/src/main/core/settings/schema.ts Adds validation for the new cleanup settings.
apps/emdash-desktop/src/main/core/settings/settings-registry.ts Adds opt-in defaults for the cleanup settings.
apps/emdash-desktop/src/main/core/tasks/operations/restoreTask.ts Updates restored tasks with a fresh interaction timestamp.
apps/emdash-desktop/src/renderer/lib/duration.ts Adds helpers for converting cleanup delays between milliseconds and UI units.
apps/emdash-desktop/src/shared/telemetry.ts Adds the typed auto-cleanup telemetry event.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  UI[General settings row] --> Settings[Task cleanup settings]
  Settings --> Scheduler[AutoCleanupScheduler]
  Scheduler --> Query[Find merged-PR task candidates]
  Query --> Decision{Cleanup action}
  Decision -->|archive| Archive[archiveTask]
  Decision -->|delete| Delete[deleteTask]
  Archive --> Telemetry[task_auto_cleaned_up]
  Delete --> Telemetry
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  UI[General settings row] --> Settings[Task cleanup settings]
  Settings --> Scheduler[AutoCleanupScheduler]
  Scheduler --> Query[Find merged-PR task candidates]
  Query --> Decision{Cleanup action}
  Decision -->|archive| Archive[archiveTask]
  Decision -->|delete| Delete[deleteTask]
  Archive --> Telemetry[task_auto_cleaned_up]
  Delete --> Telemetry
Loading
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
apps/emdash-desktop/src/main/index.ts:135
**Scheduler Runs Before Settings**

`initialize()` schedules an immediate microtask, but this call happens before `appSettingsService.initialize()` later in startup. On app launch, the first cleanup tick can read settings before persisted settings and defaults are loaded, so it can fail or skip a user-enabled cleanup until the next interval.

### Issue 2 of 5
apps/emdash-desktop/src/main/core/tasks/auto-cleanup-scheduler.ts:86-106
**System Tasks Become Candidates**

This query selects every non-archived task row with a workspace branch matching a merged PR, but it does not limit the result to normal user tasks. If an `automation-run` or other system task has a workspace on that branch, enabling auto-cleanup can archive or delete that row and trigger the normal worktree cleanup path for a task the setting did not mean to manage.

### Issue 3 of 5
apps/emdash-desktop/src/main/core/tasks/auto-cleanup-scheduler.ts:135
**Delete Always Removes Worktree**

The auto-delete path always passes `deleteWorktree: true`, while the new setting only exposes the optional branch deletion choice. When a user enables delete mode to clean up task records after merge, this path also removes the worktree even if they left the branch checkbox off, which can delete local working files earlier than the UI implies.

### Issue 4 of 5
apps/emdash-desktop/src/renderer/features/settings/components/TaskSettingsRows.tsx:287-293
**Unit Switch Drops Draft**

`updateUnit` clears the pending debounce and converts the last persisted `value`, not the number currently typed in `draftValue`. From the default `1 day`, typing `2` and immediately selecting `Hours` saves `1 hour` instead of `2 hours`, so cleanup can run much earlier than the visible input the user just entered.

### Issue 5 of 5
apps/emdash-desktop/src/shared/telemetry.ts:106-111
**Delay Field Is Stripped**

The new event type includes `delay_ms`, and the scheduler sends it, but the telemetry sanitizer's allowed property set does not include that key. Captured `task_auto_cleaned_up` events therefore lose the configured delay, so cleanup telemetry cannot answer which delay caused an automatic action.

Reviews (1): Last reviewed commit: "test(fixtures): restore upstream fixture..." | Re-trigger Greptile

Comment thread apps/emdash-desktop/src/main/index.ts Outdated
Comment thread apps/emdash-desktop/src/main/core/tasks/auto-cleanup-scheduler.ts
Comment thread apps/emdash-desktop/src/main/core/tasks/auto-cleanup-scheduler.ts
Comment thread apps/emdash-desktop/src/shared/telemetry.ts
- main/index.ts: move autoCleanupScheduler.initialize() after
  appSettingsService.initialize() so the first tick never reads the
  settings service before it has loaded persisted state.
- auto-cleanup-scheduler: restrict candidate query to tasks.type='task'
  so automation-run rows with a matching merged-PR branch are never
  picked up by user-task cleanup.
- TaskSettingsRows: when the user changes the unit while a number is
  still in the input (not yet committed), use the typed draft instead
  of the persisted value, so the saved delay matches what the user sees.
- TaskSettingsRows: clarify in the row description that 'Delete' also
  removes the worktree; only the branch deletion is optional.
- telemetry: add 'delay_ms' to the allowed-properties allowlist and
  treat it as a bounded duration; without this the sanitizer was
  silently stripping the field from task_auto_cleaned_up events.
@arnestrickmann

arnestrickmann commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Thanks for opening this PR, @SpielerNogard.

This feature makes a lot of sense, especially as a separate setting. We’ll take a closer look and get back to you with proper feedback soon.

Thanks again, also for your engagement here lately.

@SpielerNogard

Copy link
Copy Markdown
Contributor Author

Sure take your time. And all good if I can contribute to a tool i use every day im happy ☺️

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.

2 participants