Skip to content

feat(ai-vault): delete a provider session from the AI Vault list - #10249

Merged
AmethystLiang merged 19 commits into
stablyai:mainfrom
5hseok:feat/ai-vault-session-delete
Aug 7, 2026
Merged

AmethystLiang merged 19 commits into
stablyai:mainfrom
5hseok:feat/ai-vault-session-delete

Conversation

@5hseok

@5hseok 5hseok commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #9876.

Adds a Delete action to AI Vault session rows that removes a provider's session for real — moving it to the trash — behind a confirmation dialog.

AI Vault accumulates every local agent session across 16 providers, with no way to retire a finished one; they pile up and slow every scan. Deleting is the natural way to retire a session, but only if it's complete: an Orca that says "deleted" while the conversation is still on disk (or reappears on the next scan) is worse than no delete at all. So Delete is offered only where Orca can remove the session completely, and shown disabled (not hidden) elsewhere so the row's absence is never mistaken for a bug.

Supported (12 of 16 providers) — everything the session wrote lives at paths derived from the one path the scanner surfaced, and nothing there is shared with another session:

  • Single file = session (9): gemini, copilot, cursor, hermes, devin, openclaw, droid, pi, omp
  • Directory = session (3): claude, rovo, grok — claude keeps Task subagent transcripts in a sibling <uuid>/, rovo/grok keep everything under <sessionId>/, so a directory-aware delete is still complete.

Not supported (4): antigravity, kimi (dangling registry entry), codex (registry + hardlink aliases), opencode (a SQLite row, not a file). These show a disabled Delete item whose tooltip states the limit without exposing the provider's storage internals.

Design points:

  • Real removal, to the trash (shell.trashItem) — deleting another app's data must be reversible; ENOENT is treated as success so an external delete stays idempotent.
  • Renderer path is never trusted — the main process re-validates against each agent's known session roots (WSL-home expansion included) and the provider's own discovery predicate before removing anything, then invalidates the three caches that could otherwise resurrect the row.
  • Ordered removal plan — companions first, the session's own transcript last, since the transcript is what puts the row on screen; a part-way failure leaves the row to retry from rather than dropping it and stranding data on disk.
  • Claude sidecars — session-env/<uuid>/ (generated shell exports) is removed with the session; file-history/<uuid>/ (the rewind buffer holding earlier versions of the user's own files) is deliberately kept.

Screenshots

No visual change beyond the new Delete menu item, its confirmation dialog, and the disabled state for unsupported providers. (Reviewer: happy to add a short recording — the interaction is menu → Delete → confirm.)

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test (ai-vault suites green; a few unrelated suites fail only when git/electron env vars leak into the runner — GIT_ASKPASS, GIT_CONFIG_*, ELECTRON_RENDERER_URL — and pass in a clean env)
  • pnpm build
  • Added high-quality tests that would catch regressions (unit + an e2e spec that deletes real files in the isolated HOME)

Test coverage:

  • Validator: root membership, .. escape, extension/predicate mismatch, non-local host, synthetic path, directory-shaped removal plans, degenerate stems.
  • Executor: removal ordering, absent-companion idempotency, kind-mismatch rejection.
  • Renderer: row stays collapsed when a menu action is chosen (a real bug found and fixed during manual testing).
  • Manually exercised end-to-end against an isolated disposable HOME (never touching real provider data): single-file deletes, claude's 3-path delete with file-history preserved, rovo/grok directory deletes, the disabled codex tooltip, consecutive deletes, and cancel.

AI Review Report

Reviewed with an AI coding agent across the dimensions below.

  • Cross-platform (macOS/Linux/Windows): paths built with path.join/resolve; no separator assumptions. WSL-home roots are expanded for validation. The one platform gap is the trash itself — WSL UNC paths have no recycle bin, handled by the existing wsl-unc-delete path (rm -f), attempted before the lstat/realpath guards.
  • SSH/remote/local: local sessions only. SSH/runtime sessions live on the remote host and are shown disabled (non-local-host), matching the existing gating for Open/Reveal Log.
  • Agent/integration compatibility: deletability is centralized in one shared table so main and renderer can't disagree; each supported agent's removal target reuses the scanner's own root/predicate derivation, so a delete target can't drift from what discovery surfaced. The 4 unsupported providers are explicitly excluded with recorded reasons.
  • Performance: delete is a user-triggered, one-off fs op; no hot-path impact. The merge with main preserved upstream's memory-bounding of the parse-cache/list-cache (fix(memory): bound OOM-prone accumulators across Orca #10179) while keeping this branch's cache-generation guard.
  • UI quality: disabled item carries an accessible reason (tooltip + aria-label); destructive styling on the item and dialog.
  • Security (see below).

Fixed during review/testing: a leftover empty <uuid>/ directory after claude deletes (now removes the session directory itself, with a guard against a degenerate stem resolving to the project root), and rows expanding behind the confirm dialog due to React bubbling portalled menu clicks.

Security Audit

  • Untrusted IPC input: the delete payload (agent, filePath, executionHostId) is untyped at runtime and never trusted. The main process re-derives the resolved path, checks membership in the agent's known roots after resolve() collapses .., matches the provider's discovery predicate, and re-checks each removal on disk (lstat kind + realpath still inside roots) before trashing — catching a regular file reached through a symlinked parent that escapes the roots.
  • Path handling: no shell interpolation; removal goes through shell.trashItem (and the existing WSL rm -f helper for UNC paths, which refuses a bare directory and never follows a symlink).
  • Blast radius: directory-shaped deletes reject a file sitting directly in the sessions root (which would trash every session) and a degenerate stem (which would resolve to the project root).
  • No new secrets, auth, or network paths. No new dependencies.

Notes

  • Platform-specific: WSL UNC deletes fall to rm -f (no recycle bin) rather than the trash — flagged in the confirm copy.
  • Follow-up (out of scope, recorded in the design): extend deletion to SSH sessions via the ssh filesystem provider's deletePath.

Platform verification (honest scope)

  • macOS: exercised end-to-end by hand against an isolated disposable HOME — single-file deletes, claude's directory delete with file-history preserved, rovo/grok directory deletes, the disabled codex tooltip, consecutive deletes, cancel.
  • Linux: the new e2e spec (tests/e2e/ai-vault-session-delete.spec.ts) runs on CI and proves the real on-disk removal (single-file + claude directory + companions, file-history kept).
  • Windows / WSL: covered by unit tests and code review only, not a live runtime. The one place this bit — WSL-UNC directory removals falling through to shell.trashItem — was found in review and fixed (route directory removals through the WSL rm -rf branch). Reviewers may want to give the Windows/WSL path a closer look.

ELI5

AI Vault session rows gain a Delete action that really moves the provider session to trash after confirm. Finished sessions no longer pile up forever and slow every scan.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds AI Vault session deletion with shared contracts, agent/path validation, ordered filesystem trash operations, IPC and preload APIs, cache invalidation, and stale-scan guards. The renderer gains deletion eligibility checks, action menus, confirmation dialogs, toast handling, web fallbacks, and localized strings. Scanner root helpers, OpenClaw path predicates, DROID root construction, subagent listing validation, and parse-cache entry invalidation are also added or shared. Extensive tests cover validation, deletion execution, cache behavior, IPC orchestration, and renderer interactions.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the requested per-session delete flow, provider-aware validation, cache invalidation, and remote-session exclusion.
Out of Scope Changes check ✅ Passed The diff appears focused on the delete feature, related cache/path helpers, UI, IPC wiring, tests, and locale updates.
Title check ✅ Passed The title is concise and accurately summarizes the main change: deleting AI Vault provider sessions.
Description check ✅ Passed The description matches the template with all required sections and covers summary, screenshots, testing, review, security, and notes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/ai-vault/cached-session-list.ts (1)

57-79: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Keep a replacement same-key scan from being cleared by its predecessor.

After invalidation, a new request can start scan B with the same key as scan A. When A settles, the existing finally key comparison still matches and clears B’s in-flight tracking, allowing a third caller to launch another expensive scan. Guard cleanup by request identity or startGeneration, and add an A → invalidate → B → resolve A regression case.

Proposed fix
-      if (inflightKey === key) {
+      if (inflightKey === key && startGeneration === cacheGeneration) {
         inflightKey = null
         inflightList = null
       }
src/main/ipc/ai-vault.ts (1)

116-123: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Guard the multi-host in-flight cleanup with startGeneration.

invalidateMultiHostAiVaultListCache() now bumps and checks cacheGeneration, but the .finally() cleanup at line 124 still clears inflightKey/inflightList for any same-key scan. If an older scan resolves after a delete-invalidation, its cleanup can still clobber a newer same-key scan’s dedup tracking, causing an extra scan.

Proposed guard
     .finally(() => {
       // Only clear tracking if it still refers to this request: a concurrent
       // different-scope scan may have replaced it and must stay dedupable.
-      if (inflightKey === key) {
+      if (startGeneration === cacheGeneration && inflightKey === key) {
         inflightKey = null
         inflightList = null
       }
     })
🧹 Nitpick comments (2)
src/preload/index.ts (1)

3965-3966: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Tighten the aiVault preload return types.

Each aiVault IPC method is annotated as Promise<unknown>, while src/preload/api-types.ts declares specific result types like AiVaultDeleteSessionResult. This bypasses compile-time checking that the handler return shape matches the exposed API contract.

♻️ Suggested fix
-    deleteSession: (args: AiVaultDeleteSessionArgs): Promise<unknown> =>
+    deleteSession: (args: AiVaultDeleteSessionArgs): Promise<AiVaultDeleteSessionResult> =>
       ipcRenderer.invoke('aiVault:deleteSession', args),
src/renderer/src/components/right-sidebar/AiVaultSessionDeleteDialog.tsx (1)

1-1: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Ref mutated during render — prefer the conditional-setState "latest value" pattern.

Static analysis (React Doctor, no-ref-current-in-render) flags lastSessionRef.current = session as a render-time mutation. It's the common "keep last non-null value" idiom and is low-risk today, but writing to a ref unconditionally on every render (rather than only lazily/once) can misbehave with discarded/duplicated renders in Concurrent React. React's own docs recommend the equivalent pattern using state instead of a ref for exactly this "remember info from a previous render" case, which stays render-pure while producing the same flash-free result.

♻️ Proposed fix using conditional setState instead of ref mutation
-import { useRef } from 'react'
+import { useState } from 'react'
-  const lastSessionRef = useRef<AiVaultSession | null>(session)
-  if (session) {
-    lastSessionRef.current = session
-  }
-  const displayedSession = session ?? lastSessionRef.current
+  const [lastSession, setLastSession] = useState<AiVaultSession | null>(session)
+  if (session && session !== lastSession) {
+    setLastSession(session)
+  }
+  const displayedSession = session ?? lastSession

Also applies to: 31-35

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e512fe41-57b3-48fc-ae55-4e3e23e95987

📥 Commits

Reviewing files that changed from the base of the PR and between 43f6265 and cb52731.

📒 Files selected for processing (39)
  • src/main/ai-vault/cached-session-list.test.ts
  • src/main/ai-vault/cached-session-list.ts
  • src/main/ai-vault/session-delete-target.test.ts
  • src/main/ai-vault/session-delete-target.ts
  • src/main/ai-vault/session-delete.test.ts
  • src/main/ai-vault/session-delete.ts
  • src/main/ai-vault/session-scanner-discovery.ts
  • src/main/ai-vault/session-scanner-droid-kimi-sources.ts
  • src/main/ai-vault/session-scanner-parse-cache.ts
  • src/main/ai-vault/session-scanner-root-dirs.ts
  • src/main/ai-vault/session-scanner-source-discovery.ts
  • src/main/ipc/ai-vault-delete.ts
  • src/main/ipc/ai-vault-subagent-list.ts
  • src/main/ipc/ai-vault.test.ts
  • src/main/ipc/ai-vault.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/right-sidebar/AiVaultPanel.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionDeleteDialog.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionRow.test.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx
  • src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx
  • src/renderer/src/components/right-sidebar/SessionRowTrailingActions.test.tsx
  • src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-deletability.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-delete-action.test.tsx
  • src/renderer/src/components/right-sidebar/ai-vault-session-delete-action.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-delete-reason.test.ts
  • src/renderer/src/components/right-sidebar/ai-vault-session-delete-reason.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/ai-vault-session-deletion.ts
  • src/shared/ai-vault-types.ts

@5hseok

5hseok commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — both addressed in 1834a80:

  1. Unhandled IPC rejection: handleConfirmDelete now wraps the await in a catch that shows the same generic failure toast. The main handler resolves with a failed/rejected outcome rather than throwing, but a transport/serialization reject can still escape — and the caller fires it with void, so that would have been an unhandled rejection with no toast.
  2. Mid-delete dismissal: handleDialogOpenChange now ignores close requests while deletingSession is true, so Radix's Escape/outside-click/X can't dismiss an in-flight delete out from under itself.

Both are covered by regression tests (verified failing without the fix).

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@5hseok Thanks for addressing both issues and adding regression coverage. The defensive IPC rejection handling and the in-flight dialog-dismissal guard resolve the concerns.

🐇✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@AmethystLiang
AmethystLiang self-requested a review July 24, 2026 01:01
@AmethystLiang

Copy link
Copy Markdown
Contributor

reviewing now!

@nwparker nwparker added the enhancement New feature or request label Jul 27, 2026
@5hseok
5hseok requested a review from brennanb2025 as a code owner July 31, 2026 00:34
Comment thread src/main/ai-vault/session-delete.ts Resolved
Comment thread src/main/ipc/ai-vault-delete.ts Resolved
@greptile-apps

greptile-apps Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR adds a Delete action to AI Vault session rows, moving sessions to the OS trash after a confirmation dialog. It supports 12 of 16 providers (single-file and directory-shaped sessions), shows a disabled item with a tooltip for the 4 unsupported providers, and gates on live-session state, execution host, and path type before exposing the action.

  • Main-process executor (session-delete-target.ts, session-delete.ts) re-validates every IPC payload against per-agent known roots using resolve() + realpath() guards, checks disk kind (lstat), and removes companions before the transcript so a partial failure leaves the session row visible for retry.
  • Cache coherence is maintained via a cacheGeneration counter in both the multi-host cache (ai-vault.ts) and the shared local cache (cached-session-list.ts); an in-flight scan started before an invalidation cannot write its stale result back.
  • Renderer layer computes deletability once per row (shared between dropdown and context menus) and gates on host, synthetic path, agent support, and live-session state in that order, deliberately ordering the session-live check last so a supported-but-running session shows an actionable "wait for it to finish" reason rather than a permanent one.

Confidence Score: 4/5

Safe to merge; the implementation is well-guarded on all code paths exercised by tests, with the one honest gap being Windows/WSL runtime validation, which is acknowledged and scoped to unit-only coverage.

The delete path is thoroughly tested (unit + e2e on Linux CI) and follows defense-in-depth: renderer UI gate, main-process path re-validation, disk-level kind + realpath checks, and cache-generation guards to prevent deleted sessions from resurfacing. The code path exercised least at runtime is Windows/WSL — specifically the WSL UNC directory-removal branch and the interaction of WSL home expansions with the realpath root checks. The author acknowledges this gap and recommends reviewers give that path a closer look before merging on Windows.

Files Needing Attention: The WSL UNC directory removal path in src/main/ai-vault/session-delete.ts (the tryDeleteWslUncPath branch with recursive: true) and the WSL home expansion in src/main/ai-vault/session-delete-target.ts would benefit from a runtime check on Windows before merging if WSL support is a near-term priority.

Important Files Changed

Filename Overview
src/main/ai-vault/session-delete-target.ts Pure validator that re-derives per-agent session roots from the same constants the scanner uses, checks path membership and file predicates, and builds an ordered removal plan. Degenerate-stem guard for claude prevents trashing the whole project directory.
src/main/ai-vault/session-delete.ts Executor that runs the removal plan: tries WSL UNC fast path first, then lstat (kind check, symlink-safe), then realpath (roots re-realpath'd to handle symlinked-parent escape), then shell.trashItem. ENOENT treated as success throughout for idempotency.
src/main/ipc/ai-vault-delete.ts IPC adapter: fetches WSL homes, delegates to executor, and on success invalidates all three caches. Parse-cache invalidation deliberately uses the raw renderer-echoed filePath (not resolve()-d) to match the scanner's own key.
src/shared/ai-vault-session-deletion.ts Shared type and constant module. AI_VAULT_DELETABLE_AGENTS is the single source of truth used by both renderer and main, preventing disagreement on what is deletable.
src/main/ai-vault/cached-session-list.ts Adds cacheGeneration counter to prevent a stale in-flight scan from overwriting the cache after a delete invalidation. Logic is correct.
src/main/ipc/ai-vault.ts Registers the delete handler; adds the same cacheGeneration guard for the multi-host list cache. The aiVaultDeleteDeps const references a hoisted function declaration — valid JS, correctly noted in a comment.
src/renderer/src/components/right-sidebar/ai-vault-session-deletability.ts Renderer-side UI gate. Correctly checks session-live state last. The renderer gate is a subset of the main-side gate, as required.
src/renderer/src/components/right-sidebar/ai-vault-session-delete-action.ts Custom hook owning the confirm-delete flow: in-flight guard, IPC call, toast on settle, force-refresh. Error handling covers both rejected/failed outcomes and IPC transport errors.
src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx Computes deletability once and passes it to both dropdown and context menus. Adds the portalled-menu click bubble-cancellation fix.
src/renderer/src/components/right-sidebar/AiVaultSessionDeleteDialog.tsx Confirmation dialog with lastSessionRef pattern to prevent blank flash during Radix close animation. Disabled buttons during in-flight delete.
tests/e2e/ai-vault-session-delete.spec.ts E2E spec against isolated HOME: covers single-file delete (gemini) and directory-shaped delete with companions (claude) including the file-history preservation assertion.
src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.tsx Adds Delete item with accessible aria-label, wrapping div for tooltip hover over disabled item, and tooltip content explaining the limit without leaking storage internals.
src/renderer/src/web/web-preload-api.ts Web stub for deleteSession correctly returns a non-local-host rejection rather than throwing or pretending to delete.

Reviews (2): Last reviewed commit: "fix(ai-vault): address review findings o..." | Re-trigger Greptile

@AmethystLiang

Copy link
Copy Markdown
Contributor

reviewing now

WYK15 added a commit to WYK15/orca that referenced this pull request Aug 4, 2026
@AmethystLiang
AmethystLiang force-pushed the feat/ai-vault-session-delete branch from 1cbaa8b to 31c884c Compare August 5, 2026 16:30
5hseok and others added 13 commits August 7, 2026 10:54
…ders

Add the pure judgement layer for deleting an Agent Session History entry.
`validateAiVaultSessionDeleteTarget` decides whether a session may be removed:
the agent must be one of the nine providers where a single file is the whole
session (gemini, copilot, cursor, hermes, devin, openclaw, droid, pi, omp),
the host must be local, and the renderer-supplied path must resolve inside
that agent's own session roots and match its discovery predicate.

To keep the delete roots from drifting from the scanner's own roots, the
WSL-expansion helper moves to session-scanner-root-dirs.ts and the OpenClaw
root derivation + session predicate become shared helpers that
discoverOpenClawFiles itself consumes.

The result is path-only and never touches the filesystem; a returned
`allowed: true` still requires an lstat/realpath re-check in the executor
(S-2) before removal, documented as a caller contract on the result type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i
Add the filesystem executor behind session deletion. It calls the S-1 path
validator, then performs the fs-side guards that validator documented it
could not: lstat().isFile() rejects a directory or symlink, and realpath is
re-fed through the validator so a regular file reached through a symlinked
parent that escapes the agent's roots is rejected too. Only then is the file
moved to the OS trash via shell.trashItem, with ENOENT treated as success so
a delete racing an external removal stays idempotent.

WSL UNC paths (no Recycle Bin) are delegated to tryDeleteWslUncPath before the
Windows-local fs guards, mirroring fs:deletePath. Any non-ENOENT error is
returned as a failure result rather than thrown, since IPC payloads are
untyped at runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i
…alidation

Wire the S-2 delete executor to an IPC endpoint and expose it on the preload
bridge. The renderer calls aiVault:deleteSession with { agent, filePath,
executionHostId }; the handler fetches WSL homes, delegates to the executor
(which re-validates and trashes), and on a real delete invalidates the caches
that could otherwise keep serving the deleted session.

Cache invalidation is generation-guarded: a scan already in flight when the
delete lands carries an older generation and must not write its pre-delete
result back into the cache. Without this, an in-flight scan resolving just
after the delete would resurrect the deleted session for the 15s TTL — and
force-refreshing the panel only masks it for the desktop, not for the paired
mobile client or runtime RPC that share the same cache module. Both the shared
local-scope cache and the desktop multi-host cache carry the guard, with
regression tests for the in-flight race.

The delete result type moves to shared/ai-vault-types.ts so the renderer can
import the same contract the executor returns. To keep ai-vault.ts within the
max-lines budget after adding the delete wiring, two cohesive pieces are
extracted to their own files: the delete orchestration (ai-vault-delete.ts)
and listAiVaultSubagentSessions (ai-vault-subagent-list.ts). The latter is the
only handler with no dependency on this module's private cache state, so it is
the one piece that moves verbatim without threading state through a seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i
Add the renderer counterpart to the main-side delete validator: given a
session, decide whether the row menu shows Delete enabled, or disabled with a
reason a tooltip can render. It reuses the shared deletable-agent set and
unsupported-reason map so the two sides can never disagree about which agents
are deletable, and reuses the existing local-host / synthetic-path renderer
helpers.

This is intentionally not a security boundary — it validates neither the path
root nor the file predicate. Those are the main process's untrusted-input
defense; the renderer only picks the affordance, and the main side re-checks
on delete regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i
…agent

The renderer deletability check runs host -> synthetic -> agent, while the
main validator runs agent -> host -> synthetic. The two layers agree only on
deletable-or-not (renderer-false is a subset of main-false), not on the reason
code a doubly-failing session carries. Document that explicitly instead of
implying the orders match, and add the antigravity case (two reason codes) so
the agentReasonCodes array shape is actually exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i
…n dialog

Wire the delete affordance into AI Vault. Both the dropdown and the context
menu gain a destructive Delete item; a session that can't be completely deleted
(remote host, synthetic OpenCode-SQLite path, or a directory/registry-backed
agent) shows the item disabled with a reason surfaced both as a tooltip and as
an aria-label so keyboard and screen-reader users learn why. Confirming opens a
dialog that names the session and states it will no longer be resumable from
the provider's own CLI, then calls the delete IPC and force-refreshes the list
for immediate feedback (the main side has already invalidated its caches).

The confirmation copy says the session "will be deleted" rather than "moved to
the trash": on Windows a WSL session is deleted with rm inside the distro (no
Recycle Bin), so promising recoverability would be a lie on that platform.

Deletability is computed once per row and shared by both menus so they can
never disagree. New pure logic — the reason-to-tooltip mapping (including the
multi-reason join) and the delete action hook's deleted/rejected/failed
branches — is covered by unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i
The disabled Delete item explained a provider's storage layout to the user
("Claude sessions can't be deleted here: stores sessions as a folder, not a
single file"). That is Orca's problem, not the reader's — the tooltip now says
which sessions are affected and stops there. The non-local-host string stays as
it was: it states scope, not a cause, and tells the user what would work.

The reason-code plumbing existed only to compose that tooltip, so
AI_VAULT_UNSUPPORTED_DELETE_REASONS, AiVaultUnsupportedDeleteReasonCode, and the
renderer result's agentReasonCodes field go with it. Why each agent is excluded
moves into the comment above AI_VAULT_DELETABLE_AGENTS, where a reader looking
up the deletable set will find it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7
…tory

These three were excluded only because the delete unit was one file. Their
sessions are directories — claude keeps Task subagent transcripts in a sibling
`<uuid>/subagents/`, rovo and grok keep everything under `<sessionId>/` — and
nothing in them is shared with another session, so a directory-aware delete is
still a complete delete. Supported goes from 9 agents to 12; the four that
remain (antigravity, kimi, codex, opencode) are blocked by a registry or a
SQLite row, which no delete unit fixes.

Validation now returns an ordered removal plan instead of a single path. Each
removal carries the kind it must be on disk and the roots its realpath must
stay inside, so the executor's guard is the same shape for a file and for a
directory. Companions come first and the transcript last: the transcript is
what puts the row on screen, so a part-way failure leaves the row to retry
from rather than dropping it and stranding the rest on disk.

Claude's `session-env/<uuid>/` goes with the transcript — it holds that
session's generated shell exports and nothing else. Its sibling
`file-history/<uuid>/` deliberately does not: it is the rewind buffer holding
earlier versions of the user's own files, and retiring a session is no reason
to take away the only copy that can restore them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7
…subagents

Deleting a claude session trashed `<uuid>/subagents/` and left `<uuid>/` behind
as an empty directory — one per deleted session, accumulating under every
project. The directory is named after the transcript, so it belongs to that
session as a whole; take it rather than the one subdirectory inside it. Still
derived from the scanner's own subagents path, so the two cannot drift.

Reaching the parent means a degenerate stem now matters: `..jsonl` passes the
extension check and its stem is `.`, which would resolve the session directory
to the project directory holding every session. Reject it instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7
Radix portals the row's dropdown and context menus out of its DOM, but React
still bubbles their clicks back through the component tree, so every menu
selection also hit the row's own click handler and expanded it. The trigger
button already stopped propagation, which is why opening the menu looked fine
and only choosing an item misbehaved.

It shows worst on Delete: the row expands behind the confirm dialog, so
cancelling leaves the list rearranged under a dialog the user just backed out
of. Toggle details only for clicks that land in the row's own subtree — that
covers the context menu and any future portalled surface, not just this one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7
…ion and mid-delete dismissal

Two robustness gaps flagged in review:

- handleConfirmDelete only branched on result.outcome. The main handler
  resolves with a 'failed'/'rejected' outcome rather than throwing, but the
  IPC invoke itself can still reject on a transport/serialization error, and
  the caller fires it with `void`. That reject would surface as an unhandled
  rejection with no toast. Catch it and show the same generic failure toast.

- handleDialogOpenChange cleared sessionPendingDelete on every open=false.
  The Cancel button is disabled mid-delete, but Radix still fires its
  Escape/outside-click/X close, which could dismiss an in-flight delete out
  from under itself. Ignore close requests while deletingSession is true.

Both covered by regression tests (verified failing without the fix).
…anch

Directory-shaped deletes (claude's subagents/session-env dirs, rovo/grok's
session dir) gated the WSL branch on kind === 'file', so on Windows a session
under a WSL distro home fell through to shell.trashItem — which can't trash a
WSL-volume item (no Recycle Bin) and throws, or worse is silently stranded when
the 9P filesystem's unreliable lstat false-reports ENOENT and the executor
treats that as success. Single-file deletes predate the directory kinds, so the
file-only gate was correct until directory removals were added.

tryDeleteWslUncPath already supports recursive removal; pass recursive for
directory removals so they take the same WSL rm path as files instead of
shell.trashItem. Covered by two regression tests (file: non-recursive,
directory: recursive), verified failing without the fix.

Also drops the internal ledger-ID references (D-*, S-*) from comments in these
two files; they pointed at a private design doc a reader can't see.
Comments across the session-delete feature cited decision/slice IDs (D-1..D-7,
S-1..S-5) from a private design document. Those references are meaningless to
anyone reading the code without that doc, so remove the IDs while keeping the
reasoning each comment carried. No behavior change.
5hseok and others added 6 commits August 7, 2026 10:55
The unit tests mock lstat/realpath/trashItem, so nothing proved the whole IPC
path actually removes files. This spec seeds sessions into the E2E harness's
isolated HOME and deletes them through window.api.aiVault.deleteSession:

- a single-file session (gemini): the transcript is gone from disk and drops
  out of the list.
- a directory-shaped session (claude): the transcript, the <uuid>/ session
  directory (subagents included, no empty shell left), and the session-env
  companion are all gone, while the file-history rewind buffer is preserved.

Verified failing when the executor's removal is stubbed out. Runs on Linux CI.
Three points raised in review:

- Disable Delete for a still-running session. resolveAiVaultSessionDeletability
  now gates on liveState (working/blocked/waiting) last — an otherwise-deletable
  session that is mid-run shows "wait for it to finish" instead of an enabled
  Delete, so trashing a live agent's transcript can't drop writes it is still
  appending. Unsupported/remote sessions keep their permanent reason.

- Realpath the roots, not just the target, in the executor's escape check. The
  roots were only resolve()'d (text), so a session under a symlinked root
  (~/.claude -> /Volumes/…) was falsely rejected; realpath each root (falling
  back to its text form when it can't be resolved) before the membership check.

- Invalidate the parse cache with the raw filePath, not resolve(filePath). The
  cache is keyed by the exact path the scanner discovered, so resolve() could
  normalise it away from the stored key and miss. Drops the now-unused import.

Also moves AiVaultDeleteSessionArgs/Result out of ai-vault-types.ts (which the
upstream merge pushed over the max-lines limit) into the ai-vault-session-deletion
domain module they belong to, and updates importers.

Regression tests added for the live gate, the symlinked-root accept, and the
reason string; verified failing without each fix.
The bridge declared Promise<unknown> while AiVaultApi.deleteSession promises
AiVaultDeleteSessionResult, so the preload object leaned on the api-types
declaration to stay honest instead of being checked against it.

Co-authored-by: Orca <help@stably.ai>
Comments across the delete flow explained HOW alongside WHY and ran to a dozen
lines; they now carry only the non-obvious reasoning. The excluded-agent
rationale, the caller contract on the validator, and the file-history carve-out
are kept — those are knowledge, not narration.

Also removes three duplications the feature introduced:
- AiVaultSessionDeleteExecutionResult was an alias for AiVaultDeleteSessionResult
  whose comment pointed at a module the type no longer lives in.
- The synthetic-path predicate existed twice under near-identical names; the
  renderer now re-exports the shared one it already had a sibling import of.
- The delete-failure toast was written out verbatim in both the rejected and
  the thrown branch.

Co-authored-by: Orca <help@stably.ai>
… selector

The confirm dialog pinned an arbitrary sm:max-w-[440px]; every other dialog in
the right sidebar uses a scale token, and md (448px) covers the role.

The row-expand test selected the row by [draggable="true"], which stopped
naming the row when draggable moved to the title element upstream. It still
passed by bubbling, so the comment was the only thing wrong — now it selects
the title deliberately and says why the query is first-match (Radix's asChild
trigger repeats the subtree, so screen.get* sees duplicates).

Also types the e2e delete helper as AiVaultDeleteSessionResult instead of a
hand-written { outcome: string }, now that the preload bridge returns it.

Co-authored-by: Orca <help@stably.ai>
Discovery and deletion now share the same agent source definitions, eliminating the risk of them drifting apart. A single `AI_VAULT_AGENT_SOURCES` table declares each agent's root directories, file extensions, and acceptance predicates. Replaced the custom delete confirmation dialog with the system dialog, simplifying the delete action hook and removing boilerplate state management.
@AmethystLiang
AmethystLiang force-pushed the feat/ai-vault-session-delete branch from 8c2c5a5 to c1bbf95 Compare August 7, 2026 18:36
@AmethystLiang
AmethystLiang merged commit a77002c into stablyai:main Aug 7, 2026
46 checks passed
smwbev added a commit to smwbev/orca that referenced this pull request Aug 7, 2026
Resolves the conflict with stablyai#10249's table refactor: per-agent discovery
moved from session-scanner-source-discovery.ts into the
AI_VAULT_AGENT_SOURCES table, so the Prime Agent source becomes a table
entry following the Pi shape (env resolution via
primeAgentSessionsDirFromEnv unchanged). Prime Agent stays out of
AI_VAULT_DELETABLE_AGENTS. Also picks up stablyai#12999 (unknown-agent
tolerance in session-list validation) with no overlap.
zpyoung pushed a commit to zpyoung/orca that referenced this pull request Aug 9, 2026
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…blyai#10249)

* feat(ai-vault): validate session-delete targets for single-file providers

Add the pure judgement layer for deleting an Agent Session History entry.
`validateAiVaultSessionDeleteTarget` decides whether a session may be removed:
the agent must be one of the nine providers where a single file is the whole
session (gemini, copilot, cursor, hermes, devin, openclaw, droid, pi, omp),
the host must be local, and the renderer-supplied path must resolve inside
that agent's own session roots and match its discovery predicate.

To keep the delete roots from drifting from the scanner's own roots, the
WSL-expansion helper moves to session-scanner-root-dirs.ts and the OpenClaw
root derivation + session predicate become shared helpers that
discoverOpenClawFiles itself consumes.

The result is path-only and never touches the filesystem; a returned
`allowed: true` still requires an lstat/realpath re-check in the executor
(S-2) before removal, documented as a caller contract on the result type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i

* feat(ai-vault): move a validated session transcript to the trash

Add the filesystem executor behind session deletion. It calls the S-1 path
validator, then performs the fs-side guards that validator documented it
could not: lstat().isFile() rejects a directory or symlink, and realpath is
re-fed through the validator so a regular file reached through a symlinked
parent that escapes the agent's roots is rejected too. Only then is the file
moved to the OS trash via shell.trashItem, with ENOENT treated as success so
a delete racing an external removal stays idempotent.

WSL UNC paths (no Recycle Bin) are delegated to tryDeleteWslUncPath before the
Windows-local fs guards, mirroring fs:deletePath. Any non-ENOENT error is
returned as a failure result rather than thrown, since IPC payloads are
untyped at runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i

* feat(ai-vault): delete-session IPC handler, preload bridge, cache invalidation

Wire the S-2 delete executor to an IPC endpoint and expose it on the preload
bridge. The renderer calls aiVault:deleteSession with { agent, filePath,
executionHostId }; the handler fetches WSL homes, delegates to the executor
(which re-validates and trashes), and on a real delete invalidates the caches
that could otherwise keep serving the deleted session.

Cache invalidation is generation-guarded: a scan already in flight when the
delete lands carries an older generation and must not write its pre-delete
result back into the cache. Without this, an in-flight scan resolving just
after the delete would resurrect the deleted session for the 15s TTL — and
force-refreshing the panel only masks it for the desktop, not for the paired
mobile client or runtime RPC that share the same cache module. Both the shared
local-scope cache and the desktop multi-host cache carry the guard, with
regression tests for the in-flight race.

The delete result type moves to shared/ai-vault-types.ts so the renderer can
import the same contract the executor returns. To keep ai-vault.ts within the
max-lines budget after adding the delete wiring, two cohesive pieces are
extracted to their own files: the delete orchestration (ai-vault-delete.ts)
and listAiVaultSubagentSessions (ai-vault-subagent-list.ts). The latter is the
only handler with no dependency on this module's private cache state, so it is
the one piece that moves verbatim without threading state through a seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i

* feat(ai-vault): renderer judgement for whether Delete is offered

Add the renderer counterpart to the main-side delete validator: given a
session, decide whether the row menu shows Delete enabled, or disabled with a
reason a tooltip can render. It reuses the shared deletable-agent set and
unsupported-reason map so the two sides can never disagree about which agents
are deletable, and reuses the existing local-host / synthetic-path renderer
helpers.

This is intentionally not a security boundary — it validates neither the path
root nor the file predicate. Those are the main process's untrusted-input
defense; the renderer only picks the affordance, and the main side re-checks
on delete regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i

* docs(ai-vault): correct deletability parity claim; test multi-reason agent

The renderer deletability check runs host -> synthetic -> agent, while the
main validator runs agent -> host -> synthetic. The two layers agree only on
deletable-or-not (renderer-false is a subset of main-false), not on the reason
code a doubly-failing session carries. Document that explicitly instead of
implying the orders match, and add the antigravity case (two reason codes) so
the agentReasonCodes array shape is actually exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i

* feat(ai-vault): add Delete to the session row menu with a confirmation dialog

Wire the delete affordance into AI Vault. Both the dropdown and the context
menu gain a destructive Delete item; a session that can't be completely deleted
(remote host, synthetic OpenCode-SQLite path, or a directory/registry-backed
agent) shows the item disabled with a reason surfaced both as a tooltip and as
an aria-label so keyboard and screen-reader users learn why. Confirming opens a
dialog that names the session and states it will no longer be resumable from
the provider's own CLI, then calls the delete IPC and force-refreshes the list
for immediate feedback (the main side has already invalidated its caches).

The confirmation copy says the session "will be deleted" rather than "moved to
the trash": on Windows a WSL session is deleted with rm inside the distro (no
Recycle Bin), so promising recoverability would be a lie on that platform.

Deletability is computed once per row and shared by both menus so they can
never disagree. New pure logic — the reason-to-tooltip mapping (including the
multi-reason join) and the delete action hook's deleted/rejected/failed
branches — is covered by unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDLggjSAjDnaWi3Y8U622i

* fix(ai-vault): state that Delete is unavailable without naming the cause

The disabled Delete item explained a provider's storage layout to the user
("Claude sessions can't be deleted here: stores sessions as a folder, not a
single file"). That is Orca's problem, not the reader's — the tooltip now says
which sessions are affected and stops there. The non-local-host string stays as
it was: it states scope, not a cause, and tells the user what would work.

The reason-code plumbing existed only to compose that tooltip, so
AI_VAULT_UNSUPPORTED_DELETE_REASONS, AiVaultUnsupportedDeleteReasonCode, and the
renderer result's agentReasonCodes field go with it. Why each agent is excluded
moves into the comment above AI_VAULT_DELETABLE_AGENTS, where a reader looking
up the deletable set will find it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7

* feat(ai-vault): delete claude, rovo, and grok sessions by their directory

These three were excluded only because the delete unit was one file. Their
sessions are directories — claude keeps Task subagent transcripts in a sibling
`<uuid>/subagents/`, rovo and grok keep everything under `<sessionId>/` — and
nothing in them is shared with another session, so a directory-aware delete is
still a complete delete. Supported goes from 9 agents to 12; the four that
remain (antigravity, kimi, codex, opencode) are blocked by a registry or a
SQLite row, which no delete unit fixes.

Validation now returns an ordered removal plan instead of a single path. Each
removal carries the kind it must be on disk and the roots its realpath must
stay inside, so the executor's guard is the same shape for a file and for a
directory. Companions come first and the transcript last: the transcript is
what puts the row on screen, so a part-way failure leaves the row to retry
from rather than dropping it and stranding the rest on disk.

Claude's `session-env/<uuid>/` goes with the transcript — it holds that
session's generated shell exports and nothing else. Its sibling
`file-history/<uuid>/` deliberately does not: it is the rewind buffer holding
earlier versions of the user's own files, and retiring a session is no reason
to take away the only copy that can restore them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7

* fix(ai-vault): remove a claude session's own directory, not just its subagents

Deleting a claude session trashed `<uuid>/subagents/` and left `<uuid>/` behind
as an empty directory — one per deleted session, accumulating under every
project. The directory is named after the transcript, so it belongs to that
session as a whole; take it rather than the one subdirectory inside it. Still
derived from the scanner's own subagents path, so the two cannot drift.

Reaching the parent means a degenerate stem now matters: `..jsonl` passes the
extension check and its stem is `.`, which would resolve the session directory
to the project directory holding every session. Reject it instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7

* fix(ai-vault): keep a session row collapsed when a menu action is chosen

Radix portals the row's dropdown and context menus out of its DOM, but React
still bubbles their clicks back through the component tree, so every menu
selection also hit the row's own click handler and expanded it. The trigger
button already stopped propagation, which is why opening the menu looked fine
and only choosing an item misbehaved.

It shows worst on Delete: the row expands behind the confirm dialog, so
cancelling leaves the list rearranged under a dialog the user just backed out
of. Toggle details only for clicks that land in the row's own subtree — that
covers the context menu and any future portalled surface, not just this one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGuzChimmQ1dYX2raecrH7

* fix(ai-vault): harden the delete-confirmation flow against IPC rejection and mid-delete dismissal

Two robustness gaps flagged in review:

- handleConfirmDelete only branched on result.outcome. The main handler
  resolves with a 'failed'/'rejected' outcome rather than throwing, but the
  IPC invoke itself can still reject on a transport/serialization error, and
  the caller fires it with `void`. That reject would surface as an unhandled
  rejection with no toast. Catch it and show the same generic failure toast.

- handleDialogOpenChange cleared sessionPendingDelete on every open=false.
  The Cancel button is disabled mid-delete, but Radix still fires its
  Escape/outside-click/X close, which could dismiss an in-flight delete out
  from under itself. Ignore close requests while deletingSession is true.

Both covered by regression tests (verified failing without the fix).

* fix(ai-vault): route WSL UNC directory removals through the WSL rm branch

Directory-shaped deletes (claude's subagents/session-env dirs, rovo/grok's
session dir) gated the WSL branch on kind === 'file', so on Windows a session
under a WSL distro home fell through to shell.trashItem — which can't trash a
WSL-volume item (no Recycle Bin) and throws, or worse is silently stranded when
the 9P filesystem's unreliable lstat false-reports ENOENT and the executor
treats that as success. Single-file deletes predate the directory kinds, so the
file-only gate was correct until directory removals were added.

tryDeleteWslUncPath already supports recursive removal; pass recursive for
directory removals so they take the same WSL rm path as files instead of
shell.trashItem. Covered by two regression tests (file: non-recursive,
directory: recursive), verified failing without the fix.

Also drops the internal ledger-ID references (D-*, S-*) from comments in these
two files; they pointed at a private design doc a reader can't see.

* docs(ai-vault): drop internal design-ledger IDs from shipped comments

Comments across the session-delete feature cited decision/slice IDs (D-1..D-7,
S-1..S-5) from a private design document. Those references are meaningless to
anyone reading the code without that doc, so remove the IDs while keeping the
reasoning each comment carried. No behavior change.

* test(ai-vault): e2e-cover the real on-disk session delete

The unit tests mock lstat/realpath/trashItem, so nothing proved the whole IPC
path actually removes files. This spec seeds sessions into the E2E harness's
isolated HOME and deletes them through window.api.aiVault.deleteSession:

- a single-file session (gemini): the transcript is gone from disk and drops
  out of the list.
- a directory-shaped session (claude): the transcript, the <uuid>/ session
  directory (subagents included, no empty shell left), and the session-env
  companion are all gone, while the file-history rewind buffer is preserved.

Verified failing when the executor's removal is stubbed out. Runs on Linux CI.

* fix(ai-vault): address review findings on the session-delete flow

Three points raised in review:

- Disable Delete for a still-running session. resolveAiVaultSessionDeletability
  now gates on liveState (working/blocked/waiting) last — an otherwise-deletable
  session that is mid-run shows "wait for it to finish" instead of an enabled
  Delete, so trashing a live agent's transcript can't drop writes it is still
  appending. Unsupported/remote sessions keep their permanent reason.

- Realpath the roots, not just the target, in the executor's escape check. The
  roots were only resolve()'d (text), so a session under a symlinked root
  (~/.claude -> /Volumes/…) was falsely rejected; realpath each root (falling
  back to its text form when it can't be resolved) before the membership check.

- Invalidate the parse cache with the raw filePath, not resolve(filePath). The
  cache is keyed by the exact path the scanner discovered, so resolve() could
  normalise it away from the stored key and miss. Drops the now-unused import.

Also moves AiVaultDeleteSessionArgs/Result out of ai-vault-types.ts (which the
upstream merge pushed over the max-lines limit) into the ai-vault-session-deletion
domain module they belong to, and updates importers.

Regression tests added for the live gate, the symlinked-root accept, and the
reason string; verified failing without each fix.

* fix(ai-vault): type the deleteSession preload bridge as its real result

The bridge declared Promise<unknown> while AiVaultApi.deleteSession promises
AiVaultDeleteSessionResult, so the preload object leaned on the api-types
declaration to stay honest instead of being checked against it.

Co-authored-by: Orca <help@stably.ai>

* refactor(ai-vault): tighten the session-delete code to house style

Comments across the delete flow explained HOW alongside WHY and ran to a dozen
lines; they now carry only the non-obvious reasoning. The excluded-agent
rationale, the caller contract on the validator, and the file-history carve-out
are kept — those are knowledge, not narration.

Also removes three duplications the feature introduced:
- AiVaultSessionDeleteExecutionResult was an alias for AiVaultDeleteSessionResult
  whose comment pointed at a module the type no longer lives in.
- The synthetic-path predicate existed twice under near-identical names; the
  renderer now re-exports the shared one it already had a sibling import of.
- The delete-failure toast was written out verbatim in both the rejected and
  the thrown branch.

Co-authored-by: Orca <help@stably.ai>

* refactor(ai-vault): use a design-system dialog width and a stable row selector

The confirm dialog pinned an arbitrary sm:max-w-[440px]; every other dialog in
the right sidebar uses a scale token, and md (448px) covers the role.

The row-expand test selected the row by [draggable="true"], which stopped
naming the row when draggable moved to the title element upstream. It still
passed by bubbling, so the comment was the only thing wrong — now it selects
the title deliberately and says why the query is first-match (Radix's asChild
trigger repeats the subtree, so screen.get* sees duplicates).

Also types the e2e delete helper as AiVaultDeleteSessionResult instead of a
hand-written { outcome: string }, now that the preload bridge returns it.

Co-authored-by: Orca <help@stably.ai>

* refactor(ai-vault): consolidate agent sources and use system dialog

Discovery and deletion now share the same agent source definitions, eliminating the risk of them drifting apart. A single `AI_VAULT_AGENT_SOURCES` table declares each agent's root directories, file extensions, and acceptance predicates. Replaced the custom delete confirmation dialog with the system dialog, simplifying the delete action hook and removing boilerplate state management.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Remove an agent session from Agent Session History

4 participants