Skip to content

feat(trash): archive instead of delete with recycle bin and retention purge - #16746

Open
DeJeune wants to merge 41 commits into
mainfrom
DeJeune/archive-vs-delete-domains
Open

feat(trash): archive instead of delete with recycle bin and retention purge#16746
DeJeune wants to merge 41 commits into
mainfrom
DeJeune/archive-vs-delete-domains

Conversation

@DeJeune

@DeJeune DeJeune commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

🚨 Branch strategy — read before opening this PR

The v2 refactor has merged into main, so main is the default branch for active development (v1 and v2 code currently coexist there — expect large, breaking changes).

  • Active development (features, refactors, optimizations, fixes for the current codebase) → target main (the default base).
  • v1 maintenance (hotfixes and subsequent v1 releases) → branch from and target v1, not main.

A v1 fix does not auto-carry to main: if the same bug exists on main, open a separate forward-port PR targeting main. Before touching subsystems being replaced, read docs/references/data/ and watch for @deprecated markers — they flag code being deleted.

What this PR does

Before this PR:

  • Deleting a topic, message, agent, agent session, assistant, or painting hard-deleted the row (and cascaded its children) immediately — a mis-click was unrecoverable. The deletedAt column already existed on several tables but was only actually written by assistants.

After this PR:

  • Those six domains now archive instead of delete: the existing DELETE endpoints set deletedAt and route the row to a recycle bin (Settings → Data → Recently deleted) with per-domain restore / permanent-delete / empty-trash. ?permanent=true still hard-deletes, ?inTrash=true lists trashed rows, and POST /{resource}/:id/restore (+ bulk) recovers them.
  • Archive writes only the container row so children stay intact for lossless restore; pins/tags are purged on archive to avoid the list-JOIN hiding pins guard. Cleanup is handled by a daily trash.purge JobManager task gated by a new data.trash.retention_days preference (default 30, 0 = never), plus an trash.purge_now IpcApi command for "empty trash"; disk reclamation runs post-commit and failures are logged rather than thrown.
  • Read-path guards were added so session search/read and message parent/source existence checks skip archived rows, preventing live data from attaching under soon-to-be-purged trash.

Fixes #

Why we need it and why it was done in this way

Soft-delete plumbing (deletedAt + isNull read filters) already existed for topics/messages/agents, so archiving was the minimum-surface way to make deletes recoverable without changing renderer delete call sites.

The following tradeoffs were made:

  • Archive touches only the container row (children ride along via visibility), keeping restore lossless; pins/tags are intentionally not resurrected on restore.
  • Single messages are soft-deleted but not individually restorable (their subtree is reparented on delete); the recycle-bin granularity is topic/session.
  • Per the DataApi no-side-effects rule, delete endpoints never touch the filesystem — blob reclamation is deferred to the background purge.

The following alternatives were considered: a dedicated archivedAt column (rejected — reuses deletedAt); a .trash directory move for notes (deferred, see v2-refactor-temp/docs/archive/rfc-archive.md §4.5).

Links to places where the discussion took place: design RFC at v2-refactor-temp/docs/archive/rfc-archive.md.

Breaking changes

Deleting these entities no longer removes them immediately — they go to the recycle bin and are auto-purged after data.trash.retention_days days (default 30). Pins and tags are not restored when an item is recovered. Details in v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md.

Special notes for your reviewer

  • Notes and knowledge are intentionally out of scope (notes deferred; knowledge excluded by design).
  • Disk-blob reclamation for purged chat attachments / painting images is delegated to feat(file-manager): policy-based file entry GC with cleanup_policy and scan reaper #16727 (file_entry.cleanup_policy); both branches add migration 0018 and touch MessageService/PaintingService/TopicService/FileEntryService/orphanSweep.ts, so whichever lands second must regenerate its migration (never rename) and resolve those conflicts. See RFC §6.
  • Verified locally: typecheck:node + typecheck:web clean, i18n:check pass, db:migrations:check pass, and the touched service/handler/UI test suites green.

Checklist

  • Branch: This PR targets the correct branch — main for active development, v1 for v1 maintenance fixes
  • PR: The PR description is expressive enough and will help future contributors
  • Code: Write code that humans can understand and Keep it simple
  • Refactor: You have left the code cleaner than you found it (Boy Scout Rule)
  • Upgrade: Impact of this change on upgrade flows was considered and addressed if required
  • Documentation: A user-guide update was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior.
  • Self-review: I have reviewed my own code (e.g., via /gh-pr-review, gh pr diff, or GitHub UI) before requesting review from others

Release note

Deleting a topic, chat message, agent, agent session, assistant, or painting now moves it to a recycle bin (Settings → Data → Recently deleted) where it can be restored or permanently removed. Trashed items are automatically purged after a configurable retention period (default 30 days; set to 0 to keep forever). action required: pinned/tagged state is not restored when an item is recovered.

… purge

Route deletes for topics, messages, agents, agent sessions, assistants, and
paintings through soft-delete (deletedAt) instead of hard delete, add a
Settings recycle bin (restore / permanent-delete / empty), and a daily
trash.purge job gated by the data.trash.retention_days preference.

- Schema: painting + agent_session gain deletedAt (migration 0018).
- DataApi: existing DELETE endpoints archive by default; ?permanent=true hard
  deletes; new POST /{resource}/:id/restore + bulk restore; ?inTrash=true list.
- Archive writes only the container row (children stay intact for lossless
  restore); pins/tags purged on archive to avoid list-JOIN hiding.
- Purge: JobManager 'trash.purge' handler + IpcApi trash.purge_now, batched
  synchronous transactions, post-commit disk sweeps (failures logged, not
  thrown).
- UI: Settings > Data > recycle bin per DESIGN.md, i18n across all locales.
- Read-path guards: session search/read and message parent/source existence
  checks now filter out archived rows so live data can't attach to trash.

Note: file-blob reclamation for purged chat attachments / painting images is
delegated to PR #16727 (file_entry cleanup_policy); notes and knowledge are
out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
@DeJeune
DeJeune requested a review from a team July 5, 2026 02:00
@DeJeune
DeJeune requested a review from 0xfullex as a code owner July 5, 2026 02:00

@ousugo ousugo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 findings submitted as inline comments (3 must-fix, 1 non-blocking suggestion).

Note on Finding 1 originally flagged (AgentSessionService cascade filter): retracted after re-reading the PR diff. AgentService.deleteAgent already routes permanent: true through deleteByAgentIdTx intentionally (JSDoc at the call site: "Existence check up front (no isNull gate — trashed rows must be permanently deletable)"), and the soft-delete path uses the PR's new archiveByAgentIdTx (with isNull(deletedAt) filter at line 425-446 in the new file). This is design intent, not a bug.

Other 5 candidates refuted (sync transaction atomicity, RFC §4.2 separation, test-asserted ordering, etc.) — not surfaced to avoid noise.

}

/** Format a deleted-at timestamp as `YYYY-MM-DD HH:mm`; missing/invalid → "—". */
export function formatDeletedTime(ms: number | undefined): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A2] Library-first violation: hand-rolled padStart instead of using dayjs.

dayjs@^1.11.11 is already a runtime dependency (package.json:303) and is used by every sibling file in this directory: LocalBackupSettings.tsx:163, NutstoreSettings.tsx:189, WebDavSettings.tsx:78, S3Settings.tsx:87. TrashSettings is the lone outlier.

CLAUDE.md Operational Rules: "Library-first, custom-last: Before writing custom code, check library/framework docs for built-in options or existing solutions."

Minimal patch (verified byte-for-byte equivalent across undefined/NaN/epoch 0/local-tz dates, including locale-stable digit output):

+ import dayjs from 'dayjs'

  export function formatDeletedTime(ms: number | undefined): string {
    if (ms === undefined) return '—'
-   const date = new Date(ms)
-   if (Number.isNaN(date.getTime())) return '—'
-
-   const pad = (value: number) => value.toString().padStart(2, '0')
-   return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(
-     date.getMinutes()
-   )}`
+   if (!dayjs(ms).isValid()) return '—'
+   return dayjs(ms).format('YYYY-MM-DD HH:mm')
  }

Return type string is preserved; same import pattern as the four sibling files.

* Returns `null` when retention is 0 (keep forever) or `deletedAtMs` is missing;
* otherwise `Math.ceil` of the remaining days clamped to >= 0.
*/
export function computeDaysRemaining(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A3] Semantic mismatch: daysRemaining === 0 renders "Less than 1 day left" in an expired context.

computeDaysRemaining only returns 0 when remainingMs <= 0 (just expired or overdue), but the UI at TrashItemRow.tsx:32-39 routes daysRemaining < 1 to the days_remaining_lt_one i18n key ("Less than 1 day left" / "剩余不足 1 天") — implying time remains when actually it's expired.

Meanwhile the legitimate "< 1 day remaining but not yet expired" state has no UI representation at all.

Minimal patch: split computeDaysRemaining into two distinguishable sentinels so the UI can render the right copy.

  if (retentionDays <= 0 || deletedAtMs === undefined) return null
  const remainingMs = deletedAtMs + retentionDays * MS_PER_DAY - now
- return Math.max(0, Math.ceil(remainingMs / MS_PER_DAY))
+ if (remainingMs <= 0) return 0          // expired / overdue
+ if (remainingMs < MS_PER_DAY) return null // < 1 day remaining (not yet expired)
+ return Math.ceil(remainingMs / MS_PER_DAY)

Plus in TrashItemRow.tsx split the branch:

- {daysRemaining < 1
-   ? t('settings.data.trash.days_remaining_lt_one')
-   : t('settings.data.trash.days_remaining', { count: daysRemaining })}
+ {daysRemaining === 0
+   ? t('settings.data.trash.days_remaining_expired')
+   : daysRemaining === null
+     ? t('settings.data.trash.days_remaining_lt_one')
+     : t('settings.data.trash.days_remaining', { count: daysRemaining })}

Add settings.data.trash.days_remaining_expired ("Expired" / "已过期") to en-us.json, zh-cn.json, zh-tw.json.

}
}

const handleDelete = (item: TrashItem) =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[A4] Restore + Delete race: Delete button not gated while restore is in flight.

This race applies to all six sections (TopicTrashSection:76, AgentTrashSection:134, SessionTrashSection:197, AssistantTrashSection:262, PaintingTrashSection:326, FileTrashSection:385) because they share the same shape.

Why it's reachable end-to-end:

  • pendingRestoreId only flows to the Restore button at TrashItemRow.tsx:46 (loading={isRestoring}); the Delete button at TrashItemRow.tsx:51-60 has no disabled prop.
  • useMutation does NOT cancel in-flight requests (useDataApi.ts:530-540 is a bare await swrTrigger(...); the inFlightParamsRef warning at lines 506-522 is dev-only).
  • Restore and Delete are two distinct hook instances (different paths/methods), so the dev-only in-flight warning doesn't even cover them.

Failure trace: User clicks Restore on row X (pendingRestoreId = X.id, restore in flight). User clicks Delete on the same row X — Delete button allows the click, ConfirmDialog opens. Restore completes ("Restored" toast). User confirms Delete ("Permanently deleted" toast). End state: permanent: true routes to purgeManyByIdsTx hard-delete; row is permanently gone; user saw two contradictory toasts.

Minimal patch (symmetric with the existing Restore-button pattern):

// TrashItemRow.tsx (line ~52)
- <Button onClick={() => onDelete(item)}>
+ <Button onClick={() => onDelete(item)} disabled={isRestoring}>

isRestoring is already passed down via TrashSection.tsx:85 — extend its consumer at TrashItemRow.tsx to apply disabled to the Delete button as well, not just loading to the Restore button.

}
}

export const TopicTrashSection: FC<TrashDomainSectionProps> = ({ retentionDays, onRequestDelete }) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[B1] Suggestion (non-blocking): six section components share ~95% of TrashSection rendering shape but a useSoftDeletableSection factory would absorb only ~150-180 lines (≈40% reduction), not the ~280 → ~80 claimed in initial analysis.

Why the savings are smaller than they look: the six sections vary on six orthogonal axes that the factory would have to parameterize —

  1. pagination: cursor vs offset (4 fields vs 6)
  2. idParam: :id / :agentId / :sessionId / none — these match the backend route params (agents.ts:57 / agentSessions.ts:61 / topics.ts:52 etc.), so they're not drift, they're a backend decision the factory must absorb
  3. nameField: 5 distinct getters — topic.name, agent.name, session.name, assistant.name, painting.prompt, entry.name + entry.ext
  4. refreshPaths: sessions additionally refresh /agents/*
  5. useIpc: FileTrashSection uses ipcApi.request('file.batch_*', { ids: [...] }) instead of useMutation, with useInvalidateCache instead of mutation refresh — a real dual-branch
  6. FileTrashSection also short-circuits on entry.origin === 'internal' for deletedAt

No existing factory (useEntityListSection, useSoftDeletableSection, etc.) is present in the codebase, and this pattern is unique to TrashSettings/ — it's not a cross-page repetition.

Recommendation: defer to a follow-up refactor PR. The current copy is verbose but each section reads top-to-bottom without indirection. The factory would save ~40% LOC but introduce TS generics + dual branches + pagination-shape conversion that approach the duplication size.

DeJeune and others added 8 commits July 6, 2026 10:06
…lete-domains

Signed-off-by: suyao <sy20010504@gmail.com>
…entId

The main merge moved the Settings primitives to
@renderer/components/SettingsPrimitives and ErrorCode to
@shared/data/api/errors, and made JobContext.parentId required. Repoint the
trash settings + PaintingService test imports and add parentId to the trash
purge job-context test mock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
The main merge moved FileHandle types into src/shared/data/types/file.ts
and collapsed src/main/services/file/watcher/ into watcher.ts; repoint the
two references so build:check's doc-link check passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…omains

Replace the six stacked domain sections with a SelectDropdown category
filter (topics default) that renders exactly one section at a time, and
drop each section's now-redundant title header since the dropdown already
names the selected category.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…ctions

The context menu's single Delete action soft-deleted to the trash while its
confirm dialog claimed the operation was irreversible. Add an Archive action
(soft delete, no confirm) and repoint Delete at permanent deletion so the
existing destructive confirm copy is accurate. The delete handler takes an
archive|permanent mode; the inline quick-delete button keeps archiving.
History rows share the registry, so its Delete now hard-deletes too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…le bin

The Files page and Settings recycle bin rendered the same trashed-file data
with different empty-trash scopes. Remove the files-page trash library,
its restore/empty/permanent-delete flows, and the isTrash branches in the
list/grid/context-menu; trashed files are managed solely in Settings > Data.
Active-file deletion still archives internal files via file.batch_trash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

This PR implements soft-delete (trash/archive) for six domains: topics, messages, agents, agent sessions, assistants, and paintings.

Verified Implementation Quality:

  • Archive/restore operations correctly set/clear deletedAt on container rows while preserving children for lossless restore
  • Pins and tags are correctly purged on archive (intentionally not restored per RFC design)
  • Read-path guards consistently filter by isNull(deletedAt) across all services
  • Purge ordering follows RFC §6: topic → message → session → agent → assistant → painting → file entry
  • Batch processing (500 rows per transaction) correctly implemented
  • Disk reclamation failures are logged (not thrown) for retry on next purge run
  • Backward compatibility preserved: permanent defaults to true for agent/session deletion

No blocking issues found. The implementation correctly follows repository patterns for service layer transactions, error handling, lifecycle service patterns, and soft-delete guards.

Tests are comprehensive and verify the key behaviors including purge ordering, batch processing, and error handling.

🤖 Generated with Claude Code

Signed-off-by: suyao <sy20010504@gmail.com>
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

This PR implements soft-delete (trash/archive) for six domains: topics, messages, agents, agent sessions, assistants, and paintings.

Verified Implementation Quality:

  • Archive/restore operations correctly set/clear deletedAt on container rows while preserving children for lossless restore
  • Pins and tags are correctly purged on archive (intentionally not restored per RFC design)
  • Read-path guards consistently filter by isNull(deletedAt) across all services
  • Purge ordering follows RFC §6: topic → message → session → agent → assistant → painting → file entry
  • Batch processing (500 rows per transaction) correctly implemented
  • Disk reclamation failures are logged (not thrown) for retry on next purge run
  • Backward compatibility preserved: permanent defaults to true for agent/session deletion

No blocking issues found. The implementation correctly follows repository patterns for service layer transactions, error handling, lifecycle service patterns, and soft-delete guards.

Tests are comprehensive and verify the key behaviors including purge ordering, batch processing, and error handling.

🤖 Generated with Claude Code

DeJeune and others added 2 commits August 19, 2026 05:20
…state

The sweep read only the direct children of `{userData}/Data/Agents` and
deleted everything its keep-set did not claim. The four app-owned runtime
roots live exactly there — `.claude/`, `.pi/`, `.dsh/`, `system/` — and
`agent_workspace.path` points at grandchildren (`system/{date}/{sessionId}`),
so the first scheduled run would have removed every session workspace plus
the Claude config and credentials.

Reclamation is now claim-based: only artifacts whose name resolves to a live
row are candidates, and anything unrecognized is left alone.

- agent dirs: only uuid-named children, matched against `agent.id`
- session workspaces: `system/{date}/{sessionId}` against `agent_workspace.path`
- runtime session state: each driver reclaims its own, keyed by the resume
  tokens on surviving `agent_session_message` rows

The keep-set is re-read from the database after the purge transactions
commit, so nothing is handed out of the delete path and archived rows keep
their state for the whole retention window. A 5-minute mtime gate defers
artifacts an in-flight session is still writing.

Per-runtime layouts, each taken from the runtime that owns it:

- pi writes flat `{timestamp}_{token}.jsonl`
- dsh writes `{projectKey(cwd)}/{sessionId}/session.jsonl.zstd`, reclaimed a
  project at a time because subagent runs get their own session directories
  under ids Cherry never records and share the parent's cwd
- claude-code writes `{projects}/{cwd-slug}/{id}.jsonl` plus a `{id}/`
  subagent-transcript directory, swept only under Cherry's own config dir —
  the SDK's `deleteSession` resolves `CLAUDE_CONFIG_DIR` from the calling
  process, which in main reaches the user's personal `~/.claude`, and the
  Claude-login provider deliberately writes sessions there

Verified against a copy of a real dev instance: 51 live tokens, pi and dsh
reclaimed nothing, claude reclaimed 24 artifacts that sqlite confirms belong
to sessions no longer in the database, and the live transcripts sharing those
directories survived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Messages are never soft-deleted, so the `message` purge domain and
`MessageService.purgeExpiredTx` could not have had any rows to act on. Drop
both, along with the test that hand-seeded a state no code path produces, and
correct the RFC and breaking-changes notes that promised message recovery.

Make `permanent` opt-in everywhere. `AgentService.deleteAgentForDelivery`
(`?? true`) and the `AgentSessionService` delete family (`=== false`) still
defaulted to a hard delete, so a caller that omitted the flag destroyed data
while the identical topic call archived.

Archiving a session now detaches its bound task schedule and fails pending
cross-session deliveries, matching the delete path — a trashed session is as
unreachable as a deleted one, and senders waiting on a completion would
otherwise wait forever. Restoring an agent brings back the sessions archived
with it, matched on the shared archive timestamp so separately trashed
sessions stay in the trash.

UI:

- add the retention control; the docs told users to set it to 0 but nothing
  could change `data.trash.retention_days`
- the topic context menu's permanent delete now says so, instead of sitting
  next to Archive under the same "Delete" label and icon
- the history row's trash button archives, like its counterpart in the topic
  list, rather than hard-deleting

Also translate the 49 trash keys left in English across 10 locales, give
`retention_hint` a plural form, and remove the `file.empty_trash` route and
i18n keys orphaned by dropping the files-page trash view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Comment thread src/main/services/trash/agentOrphanSweep.ts
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

This PR implements soft-delete (trash/archive) for six domains: topics, messages, agents, agent sessions, assistants, and paintings.

Verified Implementation Quality:

  • Archive/restore operations correctly set/clear deletedAt on container rows while preserving children for lossless restore
  • Pins and tags are correctly purged on archive (intentionally not restored per RFC design)
  • Read-path guards consistently filter by isNull(deletedAt) across all services
  • Purge ordering follows RFC §6: topic → message → session → agent → assistant → painting → file entry
  • Batch processing (500 rows per transaction) correctly implemented
  • Disk reclamation failures are logged (not thrown) for retry on next purge run
  • Agent orphan sweep uses claim-based approach that never touches runtime roots (.claude, .pi, .dsh, system)
  • permanent parameter is opt-in everywhere (defaults to archive)

No blocking issues found. The implementation correctly follows repository patterns for service layer transactions, error handling, lifecycle service patterns, and soft-delete guards.

Tests are comprehensive and verify the key behaviors including purge ordering, batch processing, and edge cases like restoring an agent whose sessions were separately trashed.

🤖 Generated with Claude Code

@DeJeune
DeJeune requested a review from ousugo August 19, 2026 03:26
Both sides landed a migration at index 0011 — `0011_rare_vertigo`
(agent_channel_session) upstream and the agent_session/painting `deleted_at`
columns here. Resolved by taking main's chain and regenerating this branch's
migration as `0012_mature_miss_america` rather than renumbering it, so the
snapshot keeps a fresh id instead of forking the chain. `db:migrations:check`
passes and the regenerated SQL is identical to the original two ALTERs.

Everything else merged cleanly. None of the runtime layouts the agent orphan
sweep depends on were touched upstream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Findings

Summary: The core archive/purge/restore logic is correct and safe. No blocking data integrity issues found.

Consistency Issues Found (Warning):

Three methods in AgentSessionService.ts are missing isNull(sessionsTable.deletedAt) guards:

  1. advanceLastActivityAtTx (line 302) - Missing deletedAt guard unlike TopicService line 229
  2. ensureTraceIdTx (line 601) - Missing deletedAt guard unlike TopicService line 240
  3. reorderTx (line 1252) - Missing deletedAt guard unlike other services

Before this PR, sessions did not have deletedAt, so these methods did not need guards. Now that sessions support soft-delete, these methods should be updated for consistency.

Fix: Add isNull(sessionsTable.deletedAt) to the WHERE clauses.

Verified Safe:

  • Archive preserves children for lossless restore
  • Purge deletes in correct order
  • Orphan sweep is claim-based (does not delete unrecognized directories)
  • retention_days=0 correctly disables auto-purge

…sessions

Sessions had no `deletedAt` before this branch, so these three write paths
never needed a soft-delete filter; now that they do, they were the only ones
left mutating a trashed row where the `TopicService` counterparts refuse.

- `advanceLastActivityAtTx` bumped activity for a session in the trash. An
  in-flight turn can still persist its final message after the archive
  commits, so the write now fails the transaction instead of landing on a
  trashed row.
- `ensureTraceIdTx` minted a trace id for an archived session.
- `reorderTx` accepted an archived session as the reorder target, and neither
  it nor `reorderBatchTx` scoped `applyMoves`, so trashed rows still acted as
  ordering neighbours for live ones.

Reported by the automated reviewer on #16746; each path was checked for a
real caller first — all three are reachable (message persistence,
`AgentChatContextProvider`, and the `/agent-sessions/:id/order` endpoint).

`touchUpdatedAtTx` is deliberately left unguarded: it has no `TopicService`
counterpart to match, it only bumps metadata on a row that is already
invisible, and it runs on teardown paths where throwing would be a
regression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

This PR implements soft-delete (trash/archive) for six domains: topics, messages, agents, agent sessions, assistants, and paintings.

Verified Implementation Quality

Archive/Restore Logic:

  • Archive operations correctly set deletedAt on container rows while preserving children for lossless restore
  • Restore operations correctly clear deletedAt and match timestamps for agent+session atomic restore
  • Pins and tags are correctly purged on archive (intentionally not restored per RFC design)

Write-Path Guards:

  • All mutation methods in AgentSessionService, AgentService, and PaintingService have isNull(deletedAt) guards
  • Commit e8c874d correctly addressed the 3 missing guards flagged in the previous review:
    • advanceLastActivityAtTx (line 302) ✓
    • ensureTraceIdTx (line 601) ✓
    • reorderTx (line 1252) ✓
    • reorderBatchTx (line 1269) - scope parameter added ✓

Read-Path Guards:

  • All list endpoints correctly filter by inTrash parameter
  • All get/query methods use isNull(deletedAt) for normal listings and isNotNull(deletedAt) for trash listings

Purge Flow:

  • Purge ordering follows RFC §6: topic → session → agent → assistant → painting → file entry
  • Batch processing (500 rows per transaction) correctly implemented
  • Disk reclamation failures are logged (not thrown) for retry on next purge run
  • retention_days=0 correctly disables auto-purge

API Endpoints:

  • All delete endpoints implement archive-by-default pattern (permanent=true required for hard delete)
  • All restore endpoints correctly clear deletedAt
  • Schema validation is correct (permanent: boolean.optional())

Conclusion

No blocking issues found. The implementation is correct and complete. The previously flagged consistency issues have been addressed in commit e8c874d.

🤖 Generated with Claude Code

@cherry-ai-bot cherry-ai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cherry Review · 阻塞

1 blocker · 10 warning · 2 notice
逐条见行内评论。

以下 2 条落在本次 diff 的行内定位之外,改为摘要送达(位置见每条开头):

  • src/main/data/services/MessageService.ts:1894 建议修复:Child message/session operations do not consistently enforce that their parent topic or session is active, so stale callers can mutate archived containers and make restoration lossy. inv_20add8818aa4b4d2#c1
  • src/main/data/services/PaintingService.ts:390 建议修复:Painting, agent, and newly created session order-key calculations include archived rows in their live neighborhood scans, so archived keys can affect first/last and relative ordering. inv_20add8818aa4b4d2#c2

Comment thread src/renderer/pages/home/Tabs/components/Topics.tsx
Comment thread src/shared/data/api/schemas/topics.ts Outdated
@@ -0,0 +1 @@
export { TrashService } from './TrashService'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:The headless trash service and its private satellites do not meet the documented promotion bar for a features module, and the job handler is not organized under tasks/.

inv_20add8818aa4b4d2#c4

Comment thread src/main/services/trash/agentOrphanSweep.ts
Comment thread src/main/ai/runtime/pi/PiRuntimeDriver.ts
const restoreMutation = useMutation('POST', '/topics/:id/restore', { refresh: ['/topics', '/topics/*'] })
const deleteMutation = useMutation('DELETE', '/topics/:id', { refresh: ['/topics', '/topics/*'] })

const handleRestore = async (item: TrashItem) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Each trash section shares one template-path useMutation instance across rows, but only the currently restoring row is disabled, allowing concurrent row mutations to share and clobber mutation state.

inv_20add8818aa4b4d2#c8

Comment thread src/renderer/pages/settings/DataSettings/TrashSettings/TrashItemRow.tsx Outdated
Comment thread src/main/ipc/handlers/__tests__/ai.test.ts
Comment thread v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md Outdated
Comment thread v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md Outdated
DeJeune and others added 5 commits August 25, 2026 14:36
The keep-sets are a single snapshot, so a dir created before its claiming row
commits read as an orphan and was deleted outright. FRESHNESS_GATE_MS already
existed but only reached the runtime-session pass; route the agent-dir and
workspace-dir passes through reclaimStale so warm artifacts survive, and drop
the emptied date dir with a non-recursive rmdir that fails on repopulation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Permanent delete invalidated by-id wildcards, so SWR refetched rows that were
just purged and cached the 404 (same hazard useTopic.ts already documents);
restore keeps its wildcard since the row survives. Each section also backs
every row with one useMutation instance, so a second in-flight action clobbered
its state — freeze the section while one is pending. Also repoint the
non-existent text-foreground-muted/border-border-muted classes at real tokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…he last topic

The row button archives (recoverable) but was labelled Delete. The menu was
also inverted: permanent delete was allowed on the last topic — documented as
safe because the handler opens a fresh one — while the recoverable archive was
hidden there. Both now gate on pinned only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Bulk topic restore, permanent on the topic collection delete, purge_now.jobId,
and permanent on ai.agent.sessions.delete were all added by this branch and
have no production caller — the trash UI restores and purges one row at a time
and reads only the terminal status. Cover the routing that does ship instead:
permanent vs archive, restore, and inTrash forwarding at the handler seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…up claims

Pre-upgrade items are not all past the window — only those older than the
retention period are purged, by the daily 03:00 job rather than at upgrade,
and retention 0 skips the purge entirely. Per-item permanent delete is
DB-only and leaves blobs to a background sweep; only empty-trash and the
Files trash reclaim promptly. Also record the introducing PR and the
assistant group lost on archive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
@cherry-ai-bot
cherry-ai-bot Bot dismissed their stale review August 25, 2026 09:34

已有更新的评审结论,撤回这条 REQUEST_CHANGES

DeJeune and others added 5 commits August 25, 2026 19:13
…claude transcripts

Underscore is a legal resume-token character, so splitting the pi filename stem
on the last one truncated the token and made a live session read as an orphan —
its transcript was then reclaimed. Split on the first instead; the timestamp
prefix pi owns is dash-only. Claude's subagent dir had the mirror problem: it
was only reachable via its jsonl sibling, so it leaked once that file was gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…le entries

Every other destructive sweep stands aside on hasPendingRestore() — a staged
restore puts artifacts on disk that the live DB does not claim yet, which is
exactly the shape this sweep deletes. The retention purge had the matching
hole: it filtered on deletedAt alone, so a file trashed from the Files page
while a live painting still referenced it was hard-deleted, and the ref row
FK-cascaded, stripping the image out from under the painting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…rt residue

The purge only checked the abort signal once per domain, so a cancel landing
mid-batch or during the filesystem sweeps went unseen — and because the
terminal write matched on id alone, the late completion then overwrote a
forced 'cancelled' with 'completed'. Check between batches and before each
sweep, and let the first terminal write win. The tx-scoped purges stay silent
by design, so the handler now owes the read-model refresh: without it a 03:00
run left every open list rendering deleted rows. Surface a non-clean sweep
instead of swallowing it, so leftover blobs are not reported as success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
… paths

prompt_binding is polymorphic, so nothing but an explicit purge reclaims it.
The archive path purges bindings, but permanently deleting a live assistant
skips that path entirely, and the retention purge never purged them at all —
which also strands bindings for assistants archived before this release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
… restore refresh

Trashing an internal file, or bulk-deleting agent sessions from history, moves
rows to the recycle bin — the irreversible Delete wording promised otherwise.
Restore mutations refreshed by-id wildcards, revalidating every cached row of
the collection rather than the one restored. Also drop the permanent=true
claim from the collection DELETE contract, which no longer accepts it, and
record the prompt bindings an assistant loses on archive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>

@cherry-ai-bot cherry-ai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cherry Review · 阻塞

2 blocker · 10 warning · 2 notice
逐条见行内评论。

以下 3 条落在本次 diff 的行内定位之外,改为摘要送达(位置见每条开头):

  • src/main/data/services/MessageService.ts:1504 建议修复:Soft-deleted topics and agent sessions are not write barriers for in-flight message persistence. Topic message updates lack a live-topic predicate; session message upserts touch sessions without a deletedAt predicate, so pending-to-pending streaming writes can commit after archive while only a later terminal activity transition fails. inv_f91ca7f9c68cb3fa#c3
  • src/main/data/services/PaintingService.ts:390 建议修复:Painting create and reorder operations include archived paintings in the order-key neighborhood. Live target lookup filters deletedAt, but insertWithOrderKey and both applyMoves calls omit the live-row scope. inv_f91ca7f9c68cb3fa#c4
  • src/main/ai/agentSession/AgentSessionDeliveryService.ts:169 必须修复:Archiving a session does not settle all in-flight deliveries before the retry sweep runs. The no-argument kick discovers recoverable deliveries without filtering archived sessions; a later live-only lookup can fail an accepted delivery while archived, while restoring before retry can allow it to replay. inv_f91ca7f9c68cb3fa#c5

// otherwise go unobserved until they finish.
let reclaimed = true
try {
const report = await application.get('FileManager').runSweep()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Empty Trash can be reported and documented as complete while file reclamation remains incomplete. Entry cleanup handles only 100 candidates per pass and returns completed; FileManager does not fold entry-cleanup saturation or failures into the umbrella outcome, while the UI treats completed as success and the release note says cleanup happens immediately.

inv_f91ca7f9c68cb3fa#c0

const handleRestore = async (item: TrashItem) => {
setPendingRestoreId(item.id)
try {
await runAction('restore', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:File restore and permanent-delete actions show success even when the requested entry failed. The batch IPC returns per-ID failures without rejecting, but the trash UI ignores succeeded and failed and displays a success toast for any resolved request.

inv_f91ca7f9c68cb3fa#c1

)
const totalPages = Math.ceil(total / 50)

const restoreMutation = useMutation('POST', '/agents/:agentId/restore', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必须修复:Restoring an agent can leave its sessions visible in the stale session-trash list, after which permanent-delete from that list can hard-delete the restored session.

inv_f91ca7f9c68cb3fa#c2

Comment thread src/renderer/components/history/AgentHistoryRecords.tsx
Comment thread src/renderer/components/chat/actions/topicContextMenuActions.tsx Outdated
Comment thread v2-refactor-temp/docs/archive/rfc-archive.md Outdated
Comment thread v2-refactor-temp/docs/archive/rfc-archive.md Outdated
Comment thread v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md Outdated
Comment thread v2-refactor-temp/docs/archive/rfc-archive.md Outdated
@cherry-ai-bot
cherry-ai-bot Bot dismissed their stale review August 25, 2026 12:12

已有更新的评审结论,撤回这条 REQUEST_CHANGES

ousugo commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

This comment was translated automatically.

Review update (based on head 82a6ab40f1927ea90c33a081ec07f4b211723131)

Confirmed resolved and marked as Resolved:

  • inv_89a05627b05cce33#c0: Pi resume token containing underscores was incorrectly identified as orphan
  • #c1: Assistant permanent deletion/retention purge left behind prompt bindings
  • #c2: DataApi read model not refreshed after scheduled purge
  • #c8: File entries still persistently referenced could be removed by retention purge
  • #c9: Agent orphan sweep did not respect restore journal
  • #c11: Claude leftover subagent transcript directories cannot be reclaimed
  • #c12: Recoverable operations still use Delete wording
  • #c13: Restore uses high-cardinality entity wildcard refresh
  • #c14: Bulk topic delete contract incorrectly describes permanent=true
  • #c15: Breaking-change documentation incorrectly describes assistant restore integrity

Still unresolved:

  • #c3: Empty Trash may still show success when files are not fully reclaimed. It is the same issue as original c3 and the latest, more precise inv_f91ca7f9c68cb3fa#c0, already explained in the original thread, no duplicate submission.
  • #c10: The forced cancellation override issue has been fixed, but the sweep still cannot respond to abort during execution; after terminal DB no-op, publish/dispatch/onSettled may still be triggered repeatedly. The latter belongs to the same cancellation timeout chain as original c10, already added to the original thread, no duplicate issue opened.
  • #c4, #c5/#c6/#c7 in the review summary, and #c16 have not been addressed by this round of commits and remain open.

This round of review found no other new issues beyond existing comments. There are still real open items, so the conclusion remains Request changes.


Original Content

复审更新(基于 head 82a6ab40f1927ea90c33a081ec07f4b211723131

已确认解决并标记为 Resolved

  • inv_89a05627b05cce33#c0:Pi resume token 含下划线时被误判为 orphan
  • #c1:Assistant 永久删除/retention purge 遗留 prompt bindings
  • #c2:定时 purge 后 DataApi read model 未刷新
  • #c8:仍被持久引用的 file entry 可能被 retention purge
  • #c9:Agent orphan sweep 未遵守 restore journal
  • #c11:Claude 遗留 subagent transcript 目录无法回收
  • #c12:可恢复操作仍使用 Delete 文案
  • #c13:restore 使用高基数实体 wildcard refresh
  • #c14:bulk topic delete 契约错误描述 permanent=true
  • #c15:breaking-change 文档错误描述 assistant restore 完整性

仍未解决:

  • #c3:Empty Trash 仍可能在文件未完全回收时显示成功。它与 原 c3 和最新、更精确的 inv_f91ca7f9c68cb3fa#c0 属于同一问题,已在原线程说明,不再重复提交。
  • #c10:forced cancellation 覆盖问题已修复,但 sweep 执行中仍不能响应 abort;terminal DB no-op 后仍可能重复触发 publish/dispatch/onSettled。后者属于 原 c10 的同一取消超时链路,已补充到原线程,不另开重复问题。
  • #c4、review summary 中的 #c5/#c6/#c7 以及 #c16 尚未被本轮提交处理,保持开放。

本轮复审没有发现现有评论之外的其他新问题。当前仍有真实开放项,因此结论继续为 Request changes

DeJeune and others added 4 commits August 25, 2026 20:38
…le failures

Restoring an agent also restores the sessions archived with it, but the session
trash list was never refreshed — the stale rows still offered a purge that would
hard-delete a now-live session. The file routes resolve with per-id outcomes
rather than rejecting, so a failed restore or purge was toasted as success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…rminal state

The purge only checked the signal between DB batches, so a cancel arriving during
the directory walk waited for the whole tree; thread it through the agent sweep
and let an abort propagate instead of being logged as residue. finalizeJob also
still published and resolved a second time after the guarded terminal write
became a no-op, emitting a state that contradicted the forced cancellation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
The row action archives — permanent is never sent on that path — but it was
labelled and confirmed as an irreversible delete, and last round's history
relabel left the label and the dialog disagreeing outright. Give it its own
archive copy that says the session can be restored from the trash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
The RFC still read as an unimplemented design, pointed at a moved
naming-conventions path, called the schema and drizzle SQL throwaway, and
documented bulk restore plus a collection permanent-delete that were dropped for
having no caller. The release note promised immediate reclamation, said nothing
about restore differing by domain, and told users to do nothing right after
telling them to set retention before upgrading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>

@cherry-ai-bot cherry-ai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cherry Review · 阻塞

5 blocker · 11 warning · 1 notice
逐条见行内评论。

以下 6 条落在本次 diff 的行内定位之外,改为摘要送达(位置见每条开头):

  • src/main/data/services/AgentSessionMessageService.ts:1534 必须修复:Archiving a session can roll back when a completion-policy sender is already archived, because deletion preparation treats the archived sender as existent and writes a user message into it. inv_286279945f9c9c25#c1
  • src/main/data/services/AgentSessionMessageService.ts:1048 建议修复:Agent-session message reads and writes continue to address archived sessions through unscoped session lookups, allowing delivery and DataApi operations to mutate trash contents. inv_286279945f9c9c25#c2
  • src/main/data/services/MessageService.ts:1896 建议修复:Message creation and deletion can mutate an archived topic's message tree because topic ownership is loaded without requiring the topic to be live. inv_286279945f9c9c25#c3
  • src/main/data/services/PaintingService.ts:390 建议修复:Painting create and reorder operations calculate order keys using archived paintings, unlike the other soft-deleted ordered domains. inv_286279945f9c9c25#c4
  • src/main/ai/agentSession/AgentSessionDeliveryService.ts:590 必须修复:Delivery recovery and idle kicks continue to process accepted or delivering messages belonging to archived sessions, changing their status and breaking lossless restore. inv_286279945f9c9c25#c8
  • src/renderer/components/history/AssistantHistoryRecords.tsx:231 建议修复:History bulk-delete presents an irreversible Delete action even though its handler performs archive-only deletion. inv_286279945f9c9c25#c12

已采纳的说明(上一轮的回复经查证成立,本轮不再计入):

  • src/main/ai/runtime/pi/PiRuntimeDriver.ts:102 Accepted that Pi resume-token parsing now splits at the first underscore; the current parser and verifier verdict establish that the original token-truncation issue is resolved.
  • src/main/data/services/AssistantService.ts:654 Accepted that assistant permanent deletion and retention purge clear prompt bindings while archive retains them; the current service implementation establishes this.
  • src/main/features/trash/trashPurgeJobHandler.ts:136 Accepted that scheduled purge now emits post-commit per-domain read-model notifications; the current handler establishes this.
  • src/main/data/services/FileEntryService.ts:842 Accepted that retention purge protects file entries with persistent references; the current purge predicate establishes this.
  • src/main/features/trash/agentOrphanSweep.ts:49 Accepted that agent orphan sweeping honors the restore journal fail-safe; the current sweep guard establishes this.
  • src/main/ai/runtime/registerDrivers.ts:66 Accepted that Claude runtime reclamation treats leftover transcript directories as independently identifiable artifacts; the current driver implementation establishes this.
  • src/renderer/pages/files/FilesPage.tsx:580 Accepted that recoverable file actions use Archive wording; the current Files page establishes this.
  • src/renderer/pages/settings/DataSettings/TrashSettings/TrashDomainSections.tsx:64 Accepted that restore refresh was narrowed to specific entity paths; the current mutation configuration establishes this.
  • src/shared/data/api/schemas/topics.ts:202 Accepted that collection topic deletion is documented as archive-only; the current contract documentation establishes this.
  • v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md:16 Accepted that assistant restore documentation now states group and prompt bindings do not return; the current breaking-change document establishes this.

.get('DbService')
.getDb()
.update(sessionsTable)
.set({ deletedAt: null })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Restoring an individual agent session can leave it active while its parent agent remains archived, making it visible but unusable at runtime.

inv_286279945f9c9c25#c0

* done.
*/
export const trashPurgeJobHandler: JobHandlerFor<'trash.purge'> = {
recovery: 'singleton',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Singleton recovery treats scheduled retention purges and manual empty-trash jobs as interchangeable, so restart recovery can cancel an empty-trash operation.

inv_286279945f9c9c25#c5

Comment thread src/main/features/trash/trashPurgeJobHandler.ts Outdated
@@ -0,0 +1 @@
export { TrashService } from './TrashService'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:The trash modules are placed under features/trash even though the documented promotion rule routes this small headless capability through services/trash.

inv_286279945f9c9c25#c7

Comment thread src/main/ai/runtime/pi/PiRuntimeDriver.ts Outdated
const runAction = useTrashActionRunner()
const [pendingRestoreId, setPendingRestoreId] = useState<string | null>(null)

const { pages, isLoading, isRefreshing, error, hasNext, loadNext, refresh } = useInfiniteQuery('/topics', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必须修复:Stale paginated or cross-window trash rows can permanently delete entities that have since been restored or purged.

inv_286279945f9c9c25#c11

Comment thread v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md Outdated

## Notes for release manager

Part of the archive-instead-of-delete rollout (RFC: v2-refactor-temp/docs/archive/rfc-archive.md). One aggregated release-note entry should cover all domains (topics, assistants, agents, sessions, paintings, files — not single messages); this fragment is the canonical one — sibling workstreams intentionally do not add their own to avoid duplicates. Out of scope by design decision: knowledge (excluded) and notes (deferred, see RFC §4.5).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:The release-manager note leaves an existing file-cleanup fragment that still says deleting topics or paintings immediately reclaims files, contradicting this PR's archive-first behavior.

inv_286279945f9c9c25#c14

Comment thread src/main/features/trash/TrashService.ts Outdated
* promises during shutdown, so a request pending at quit never resolves;
* acceptable for this fire-from-UI path.
*/
async purgeNow(): Promise<{ status: TerminalJobStatus }> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必须修复:Manual empty-trash can report success after database purge while file reclamation is incomplete.

inv_286279945f9c9c25#c15

Comment thread src/main/core/job/JobManager.ts Outdated
@cherry-ai-bot
cherry-ai-bot Bot dismissed their stale review August 26, 2026 05:37

已有更新的评审结论,撤回这条 REQUEST_CHANGES

DeJeune and others added 5 commits August 26, 2026 16:11
The previous guard compared statuses, so a handler that resolved as 'cancelled'
after a forced cancel still republished and re-resolved the waiters. Have
setTerminalTx report whether it actually wrote — it only touches a non-terminal
row — and key the guard off that instead.

Also relocates trash to services/trash: three files is a topic subdirectory,
and features/ is earned only by a large multi-file domain (main-process §2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…etention off

Empty Trash toasted unqualified success: the sweeps are batch-capped and stand
aside during a restore, so the rows being gone does not mean the space came
back. Carry the job's reclaimed flag out through purgeNow and say so. Retention
0 also returned before the sweeps, which left residue from permanent deletes
stranded forever — it disables the row purge, not disk reclamation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Clearing deletedAt on a session whose agent is still archived brings it back
visible but unusable — the agent it needs is gone. Refuse it and point at the
agent restore, which brings its sessions back anyway. Also corrects two stale
contracts: the pi comment still described the last-underscore split the parser
no longer uses, and the reclaim contract called the keep-set live sessions when
it deliberately covers archived ones so restore stays lossless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
The trash lists never subscribed to DataApi change notifications, so a restore
or purge from another window left rows on screen whose permanent-delete would
hard-delete a live entity. Subscribe each section to its own collection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
…fragment

The upgrade steps told users to set trash retention to 0 before upgrading, but
that setting ships with this release. Point them at first launch instead. The
sibling file-cleanup fragment still claimed deleting a topic or painting
reclaims its files immediately, which archiving defers to the purge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
@DeJeune DeJeune linked an issue Aug 27, 2026 that may be closed by this pull request
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>

@cherry-ai-bot cherry-ai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cherry Review · 阻塞

1 blocker · 5 warning
逐条见行内评论。

以下 1 条落在本次 diff 的行内定位之外,改为摘要送达(位置见每条开头):

  • src/main/data/services/MessageService.ts:377 建议修复:Archived topics remain readable through message APIs. inv_258932a21e9b5ead#c2

已采纳的说明(上一轮的回复经查证成立,本轮不再计入):

  • src/main/ai/runtime/pi/PiRuntimeDriver.ts:102 Accepted that Pi resume-token parsing now splits at the first underscore; the current parser and verifier verdict establish that the original token-truncation issue is resolved.
  • src/main/data/services/AssistantService.ts:654 Accepted that assistant permanent deletion and retention purge clear prompt bindings while archive retains them; the current service implementation establishes this.
  • src/main/features/trash/trashPurgeJobHandler.ts:136 Accepted that scheduled purge now emits post-commit per-domain read-model notifications; the current handler establishes this.
  • src/main/data/services/FileEntryService.ts:842 Accepted that retention purge protects file entries with persistent references; the current purge predicate establishes this.
  • src/main/features/trash/agentOrphanSweep.ts:49 Accepted that agent orphan sweeping honors the restore journal fail-safe; the current sweep guard establishes this.
  • src/main/ai/runtime/registerDrivers.ts:66 Accepted that Claude runtime reclamation treats leftover transcript directories as independently identifiable artifacts; the current driver implementation establishes this.
  • src/renderer/pages/files/FilesPage.tsx:580 Accepted that recoverable file actions use Archive wording; the current Files page establishes this.
  • src/renderer/pages/settings/DataSettings/TrashSettings/TrashDomainSections.tsx:64 Accepted that restore refresh was narrowed to specific entity paths; the current mutation configuration establishes this.
  • src/shared/data/api/schemas/topics.ts:202 Accepted that collection topic deletion is documented as archive-only; the current contract documentation establishes this.
  • v2-refactor-temp/docs/breaking-changes/2026-07-04-topic-delete-moves-to-trash.md:16 Accepted that assistant restore documentation now states group and prompt bindings do not return; the current breaking-change document establishes this.

return agent
}

purgeExpiredTx(tx: DbOrTx, cutoffMs: number, limit: number): string[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必须修复:Retention purge hard-deletes agents without deleting their enabled agent.task schedules.

inv_258932a21e9b5ead#c0

.where(
and(
agentFilter,
inTrash ? isNotNull(sessionsTable.deletedAt) : isNull(sessionsTable.deletedAt),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Sessions retained when their agent is archived still appear in default active session reads.

inv_258932a21e9b5ead#c1

* the `painting_file_ref` rows, so the returned entity's files are intact.
* NOT_FOUND when the painting doesn't exist or is not in the trash.
*/
restore(id: string): Painting {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Archive and restore mutations for several trash domains do not broadcast DataApi changes, leaving other windows stale.

inv_258932a21e9b5ead#c3

{
name: 'topic',
purgeExpiredTx: (tx, cutoffMs, limit) => topicService.purgeExpiredTx(tx, cutoffMs, limit),
notifyPurged: (ids) => topicService.notifyReadModelChange(ids, 'membership')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:Trash purge notifications omit the child message read models deleted with topics and sessions.

inv_258932a21e9b5ead#c4

@@ -758,7 +768,12 @@ export class AgentSessionService {
if (reassigned && current.taskScheduleId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议修复:A failed PATCH against an archived task-bound session still clears its task relation.

inv_258932a21e9b5ead#c5

@cherry-ai-bot
cherry-ai-bot Bot dismissed their stale review August 28, 2026 00:16

已有更新的评审结论,撤回这条 REQUEST_CHANGES

Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
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.

[Feature]: Unified Recycle Bin for Deleted Items

2 participants