Skip to content

fix(reset): close all DBs before restore/reset; clear entire Data/ on factory reset#12892

Merged
kangfenmao merged 9 commits intomainfrom
fix/agents-db-busy
Feb 13, 2026
Merged

fix(reset): close all DBs before restore/reset; clear entire Data/ on factory reset#12892
kangfenmao merged 9 commits intomainfrom
fix/agents-db-busy

Conversation

@EurFelux
Copy link
Copy Markdown
Collaborator

@EurFelux EurFelux commented Feb 12, 2026

What this PR does

Before this PR:

  • On Windows, restoring a backup fails with EBUSY: resource busy or locked, unlink '...\Data\agents.db' because the DatabaseManager holds an open libsql connection.
  • After restore/reset, ApiServer keeps stale auth config in memory, causing silent 403 errors. DatabaseManager also holds a stale connection, causing 500 errors on write operations.
  • Factory reset (Settings > Data > Reset Data) only clears localStorage, IndexedDB, and Data/Files/, leaving agents.db, memories.db, KnowledgeBase/, and Notes/ intact.

After this PR:

  • DatabaseManager.close() releases the file handle before deleting the Data directory, allowing restore to succeed on Windows.
  • window.api.relaunchApp() replaces window.api.reload() after restore/reset, restarting the entire Electron process so all singletons (ApiServer, DatabaseManager, etc.) re-initialize with fresh config.
  • Factory reset now closes all DB connections (agents, memory, knowledge) and removes the entire Data/ directory, ensuring a complete data wipe. On relaunch, all services auto-recreate empty databases.
  • Backup restore also closes MemoryService and KnowledgeService connections (in addition to DatabaseManager), preventing EBUSY on Windows for memories.db and KnowledgeBase files.

Fixes #12891, Fixes #12838, Fixes #12837

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

The following tradeoffs were made:

  • Using full app relaunch instead of renderer-only reload is slightly more disruptive to the user (brief window close/reopen), but it's the only reliable way to ensure all main process singletons refresh. A renderer-only reload cannot trigger ApiServer.restart() at the right time because reduxService.select() reads from in-memory Redux state which hasn't rehydrated yet.
  • For factory reset, we delete the entire Data/ directory instead of individual files. This is simpler and more thorough — any future data files added to Data/ will automatically be covered.
  • KnowledgeService.closeAll() accesses LibSqlDb's private client field via (db as any).client.close() to release file handles. This breaks encapsulation but is necessary because LibSqlDb doesn't expose a close() method, and on Windows open handles prevent file deletion.

The following alternatives were considered:

  • Calling apiServer.restart() from the renderer before reload — doesn't work because Redux in-memory state still has old config at that point.
  • Listening for ReduxStoreReady after reload to trigger ApiServer restart — more complex, fragile, and only partially fixes the problem (other singletons would still be stale).
  • Deleting individual DB files in factory reset — fragile and requires maintenance whenever new data files are added.

Breaking changes

None. The user experience changes from a page refresh to a full app restart after restore/reset, which is a minor UX difference. Factory reset now actually clears all data as users expect.

Special notes for your reviewer

  • DatabaseManager.close() is a new public static method that closes the connection and resets the singleton. The next getInstance() call lazily re-initializes.
  • KnowledgeService.closeAll() is a new method that closes all open knowledge base DB connections and clears cached RAG application instances.
  • The relaunchApp() change affects 3 code paths in BackupService.ts: v1 restore, v2+ restore, and data reset.
  • New App_ResetData IPC channel orchestrates the complete factory reset sequence from the main process.
  • Backup/restore already copies/restores the entire Data/ directory, so agents.db, memories.db, KnowledgeBase/, and Notes/ are all properly backed up and restored.

Checklist

  • 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: Not required — internal behavior change, no user-facing documentation needed

Release note

Fixed factory reset not clearing agents.db, memories.db, KnowledgeBase, and Notes data. Fixed backup restore failure on Windows (EBUSY error) for memory and knowledge base files. Fixed auth/DB connection staleness after restore/reset.

EurFelux and others added 2 commits February 12, 2026 17:44
On Windows, open file handles prevent deletion (EBUSY). Before removing
the Data directory during backup restore, close the DatabaseManager's
libsql connection to agents.db so the file can be deleted.

Add DatabaseManager.close() static method that closes the connection
and resets the singleton. The next getInstance() call will lazily
re-initialize a fresh connection to the restored database.

Closes #12891

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace window.api.reload() (renderer-only refresh) with
window.api.relaunchApp() (full process restart) after backup restore
and data reset. This ensures main process singletons like ApiServer
re-initialize with the restored config, fixing auth mismatch (403)
and stale DB connection (500) errors.

Closes #12838

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@EurFelux
Copy link
Copy Markdown
Collaborator Author

EurFelux commented Feb 12, 2026

Note

This issue/comment/review was translated by Claude.

Note to reviewers:

Verified on macOS:

  • ✅ After restore, ApiServer auth works correctly (no silent 403)
  • ✅ After restore, agent CRUD operations work (no 500 from stale DB connection)

Needs Windows testing:

  • ⬜ Backup restore completes without EBUSY error (DatabaseManager.close() releases the file handle before fs.remove())
  • ⬜ After data reset, app relaunches and all services function normally

Original Content

Note to reviewers:

Verified on macOS:

  • ✅ After restore, ApiServer auth works correctly (no silent 403)
  • ✅ After restore, agent CRUD operations work (no 500 from stale DB connection)

Needs Windows testing:

  • ⬜ Backup restore completes without EBUSY error (DatabaseManager.close() releases the file handle before fs.remove())
  • ⬜ After data reset, app relaunches and all services function normally

…y reset

Factory reset previously only cleared localStorage, IndexedDB, and
Data/Files/. This left agents.db, memories.db, KnowledgeBase/, and
Notes/ intact. Add App_ResetData IPC that closes all database
connections (agents, memory, knowledge) before removing the entire
Data/ directory. Also close memory and knowledge DBs during backup
restore to prevent EBUSY on Windows.

Closes #12837

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@EurFelux EurFelux requested a review from 0xfullex as a code owner February 12, 2026 10:23
@EurFelux EurFelux changed the title fix(backup): close DB connection before restore and relaunch app after fix(backup): close all DBs before restore/reset; clear entire Data/ on factory reset Feb 12, 2026
@EurFelux EurFelux changed the title fix(backup): close all DBs before restore/reset; clear entire Data/ on factory reset fix(reset): close all DBs before restore/reset; clear entire Data/ on factory reset Feb 12, 2026
Copy link
Copy Markdown
Collaborator Author

@EurFelux EurFelux left a comment

Choose a reason for hiding this comment

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

Review Summary

Overall this is a well-structured PR that addresses real pain points (Windows EBUSY, stale singletons after restore, incomplete factory reset). The approach of using app.relaunch() instead of renderer-only reload is the right call, and the new App_ResetData IPC channel cleanly separates the factory reset logic.

Key findings:

  1. Error resilience in close sequences — The three close() calls in both ipc.ts and BackupManager.ts run sequentially without individual error guards. If memoryService.close() throws (it lacks internal try/catch unlike the other two), the remaining close calls and fs.promises.rm won't execute. Consider .catch() guards or Promise.allSettled.

  2. Race condition in DatabaseManager.close() — The method nulls DatabaseManager.instance last, creating a small window where concurrent getInstance() calls could return a stale instance with null client. Nulling the singleton first would be safer.

  3. (db as any).client in KnowledgeService — Acknowledged in the PR description as a necessary encapsulation break. Would benefit from a tracking issue reference and a warn-level log when client is not accessible (to catch upstream changes).

  4. Inconsistent singleton accessBackupManager.ts uses MemoryService.getInstance().close() while ipc.ts uses a module-level memoryService reference. Not a bug, but worth standardizing.

  5. Factory reset UX — Unlike the restore paths which show a success toast before relaunching with a 1s delay, the factory reset path relaunches immediately with no user feedback.

None of these are blockers — the core logic is sound and addresses the bugs correctly. The suggestions above are improvements for robustness.

@GeorgeDong32

This comment was marked as off-topic.

@EurFelux
Copy link
Copy Markdown
Collaborator Author

EurFelux commented Feb 12, 2026

Note

This issue/comment/review was translated by Claude.

PR #12892 Code Review Report

Summary of Changes

This PR introduces LAN file transfer functionality, primarily adding two new methods for creating and cleaning up temporary backup files for LAN transfer:

If this is a model hallucination, it's too serious.


Original Content

PR #12892 Code Review Report\r\n> ### Summary of Changes\r\n> This PR introduces LAN file transfer functionality, primarily adding two new methods for creating and cleaning up temporary backup files for LAN transfer:\r\n\r\n这个如果是模型幻觉的话也太严重了

…, and UX

- Wrap memoryService.close() and KnowledgeService.closeAll() with
  .catch() guards so a failure in one doesn't prevent subsequent
  close calls or fs.rm from executing
- Null DatabaseManager.instance before closing client to prevent
  concurrent getInstance() from returning a stale instance
- Log warn when LibSqlDb client is not accessible in closeAll()
  to detect upstream changes that break the (db as any).client cast
- Add toast + 1s delay before relaunch on factory reset for
  consistent UX with restore paths
- Add message.reset.success i18n key across all locales

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@GeorgeDong32
Copy link
Copy Markdown
Collaborator

GeorgeDong32 commented Feb 12, 2026

Note

This issue/comment/review was translated by Claude.

That's too serious if it's a model hallucination. That was indeed written by me 😅. I accidentally selected auto-allow when uploading the review.


Original Content

这个如果是模型幻觉的话也太严重了\r\n\r\n这就是火山 😅,Review上传不小心选成自动允许了

Copy link
Copy Markdown
Collaborator Author

@EurFelux EurFelux left a comment

Choose a reason for hiding this comment

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

Re-review: All feedback addressed ✅

All five review comments have been addressed. Summary of the fixes:

  1. Error handling ✅ — .catch() guards added on memoryService.close() and KnowledgeService.closeAll() in both ipc.ts and BackupManager.ts, ensuring fs.promises.rm always runs.

  2. KnowledgeService encapsulation ✅ — warn-level log when client is not accessible, plus a TODO comment tracking the need for upstream close() support in LibSqlDb.

  3. DatabaseManager race condition ✅ — DatabaseManager.instance is now nulled first, so concurrent getInstance() calls will create a fresh connection instead of getting a stale one.

  4. BackupManager singleton access ✅ — Author clarified: using MemoryService.getInstance() is intentional here since BackupManager is an independent class without a module-level memoryService reference. Makes sense.

  5. Factory reset UX ✅ — Now shows toast.success and uses the same setTimeout(() => relaunchApp(), 1000) pattern as restore paths. i18n translations for message.reset.success added across all locales.

LGTM. Clean, well-addressed feedback. No further concerns.

Copy link
Copy Markdown
Collaborator

@DeJeune DeJeune left a comment

Choose a reason for hiding this comment

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

Review: Gap Analysis — Are all file handles released?

The core approach is sound — relaunchApp() over reload() is the right architectural call, and the new App_ResetData IPC cleanly separates the factory reset logic. The three issues are correctly root-caused and addressed. However, I spotted two gaps that could still cause EBUSY on Windows:


1. FileStorage chokidar watcher not stopped before fs.remove() (Medium severity)

Affected paths: BackupManager.restore() (line ~413) and App_ResetData IPC handler in ipc.ts

FileStorage uses chokidar to watch Data/Files/ and Data/Notes/ directories (FileStorage.ts:1674). This watcher holds open file handles via fs.watch/FSEvents underneath. Before calling fs.remove(destPath) in BackupManager or fs.promises.rm(getDataPath()) in the reset handler, the watcher is not stopped.

On Windows, chokidar's underlying ReadDirectoryChangesW holds directory handles that will cause EBUSY when trying to delete the directory — the exact same class of bug this PR is fixing for DB connections.

FileStorage already exposes stopFileWatcher() (FileStorage.ts:1798). Consider calling it before fs.remove:

// In BackupManager.restore(), before fs.remove(destPath):
await fileStorage.stopFileWatcher()

// Similarly in App_ResetData handler, before fs.promises.rm():
await fileStorage.stopFileWatcher()

Since the app relaunches immediately after, there's no need to restart the watcher.


2. KnowledgeService.closeAll() silently skips unclosable handles (Medium severity)

Location: KnowledgeService.ts, closeAll() method

When (db as any).client is not accessible (e.g., upstream LibSqlDb changes its internals), the current code logs a warning and continues:

if (client && typeof client.close === 'function') {
  client.close()
} else {
  logger.warn(`Cannot close database instance for id: ${id} — client not accessible`)
  continue  // ← caller thinks all handles released
}

The caller (both BackupManager and App_ResetData) will proceed to fs.remove() believing all handles are released, but the skipped DBs still hold file locks. On Windows this means EBUSY — silently masking the very bug this PR fixes.

Suggestion: throw an error (or collect failures and throw at the end) so the caller is aware that not all handles were released:

} else {
  const msg = `Cannot close database instance for id: ${id} — client not accessible`
  logger.error(msg)
  throw new Error(msg)
}

Or at minimum, track failures and return a result that callers can check:

public closeAll = async (): Promise<{ failed: string[] }> => {
  const failed: string[] = []
  // ...
  if (!client || typeof client.close !== 'function') {
    failed.push(id)
  }
  // ...
  return { failed }
}

Minor note

The inconsistent singleton access pattern (MemoryService.getInstance().close() in BackupManager vs module-level memoryService in ipc.ts) is fine given the different module contexts, just calling it out for awareness.


Summary

The PR correctly fixes the root causes of all three issues. The two gaps above are worth addressing before merge to avoid residual EBUSY failures on Windows, particularly the FileStorage watcher which is a near-certain issue when the Notes panel has been opened during the session.

EurFelux and others added 2 commits February 13, 2026 01:39
Address review feedback from DeJeune:

1. Stop FileStorage chokidar watcher before fs.remove() in both
   App_ResetData handler and BackupManager.restore(). On Windows,
   chokidar's ReadDirectoryChangesW holds directory handles that
   cause EBUSY when deleting Data/.

2. KnowledgeService.closeAll() now collects failed close attempts
   and throws an error instead of silently skipping. Callers are
   already guarded with .catch() so they'll log the warning and
   proceed with fs.remove() — the subsequent relaunch will release
   any remaining handles via process exit.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@EurFelux EurFelux requested a review from DeJeune February 12, 2026 17:42
@EurFelux
Copy link
Copy Markdown
Collaborator Author

EurFelux commented Feb 12, 2026

Follow-up: Address DeJeune's review + deduplicate close logic

Commits: 810cf85b92bbe0

1. Stop FileStorage watcher before fs.remove() (810cf85)

Addresses DeJeune's review point about chokidar. FileStorage uses chokidar to watch Data/Files/ and Data/Notes/. On Windows, ReadDirectoryChangesW holds directory handles that cause EBUSY. Now stopFileWatcher() is called before deleting the Data directory in both App_ResetData and BackupManager.restore().

2. KnowledgeService.closeAll() throws on unclosable handles (810cf85)

Addresses DeJeune's review point about silent skipping. Instead of silently skipping when client is not accessible, closeAll() now collects failed IDs and throws Error. Callers already have .catch() guards so they log and proceed.

3. Extract closeAllDataConnections() to src/main/utils/lifecycle.ts (df0572d)

The 4-step close sequence (DatabaseManager → MemoryService → KnowledgeService → FileWatcher) was duplicated in ipc.ts and BackupManager.ts. Extracted to a shared function using Promise.allSettled — all closes run in parallel, failures in one don't block others.

4. Best-effort fs.rm() in factory reset (b92bbe0)

If fs.rm() fails (e.g., a handle wasn't released on Windows), the error is logged but not thrown. This ensures relaunchApp() always executes — process exit releases all remaining handles, and services auto-recreate on next start.

@kangfenmao kangfenmao merged commit fc7e374 into main Feb 13, 2026
9 checks passed
@kangfenmao kangfenmao deleted the fix/agents-db-busy branch February 13, 2026 03:24
@GeorgeDong32 GeorgeDong32 linked an issue Feb 14, 2026 that may be closed by this pull request
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants