You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Upgrading is brittle on large database #17937
apps/server
Why T3 errors: at 21:22 UTC your desktop (updated to nightly 20261010) asked the server
to update itself. While the service launcher was file-copying the 5.5 GB state database
as its backup, the desktop's SSH launcher started a second t3 serve on port 3773. The
updated server then died with EADDRINUSE, and the launcher "rolled back" by copying that
backup over the live database while the second server had it open. statev2.sqlite has
been malformed since, so every thread-list read fails.
Impact
Major degradation or frequent failure
Version or commit
nightly 20261010
Workaround
Claude Opus 5.5:
I fixed everything fixable. Two findings have no fix:
Restart race: while the unit starts, your desktop can launch a second server for an instant. T3 has no lock both launch paths honour, so every restart of t3code.service already has this window; the script narrows it.
The lost rows above: worst case, one message that was rejected during the failed update gets accepted if the client retries it.
Same failure here, with a worse outcome: the database came back empty, not malformed. All threads, projects and scheduled tasks were gone. Adding our timeline and the code paths involved, since it pins down the exact race.
Setup: Linux VM, t3code.service (service launcher from 0.0.46-nightly.20261010.2908), desktop on macOS connected over SSH. Update 0.0.46-nightly.20261011.2955 → 0.0.46-nightly.20261013.3042, statev2.sqlite several hundred MB, slow disk (IO pressure "full" 36–64 %).
Timeline (local time):
09:39:46: the active server hands off to the launcher. The launcher stops it, and backupDatabaseOnce starts copying the DB file. This took ~5 minutes on the slow disk, and port 3773 was down the whole time.
09:40–09:45: the desktop's SSH route found nothing on 3773 and started its own t3 serve --port 3773 --base-dir ~/.t3 against the same DB. We run a watchdog that kills foreign t3 serve processes on that base dir, so this repeated 5 times (one new server per minute). The first ones served the old, intact DB (their logs show the old threads).
09:44:54: backup done, the trial child starts and fails: listen EADDRINUSE 127.0.0.1:3773 (a desktop-launched server holds the port).
09:44:5x: #handleExit → #returnToPrevious → restoreDatabaseBackup runs copyFile(backup, statev2.sqlite) over the live file (same inode, truncate + write).
09:44:59: the next desktop-launched server opened the DB mid-restore, saw an empty file, and ran all 61 migrations from scratch. Every row in effect_sql_migrations is stamped with that second.
Right after: discardDatabaseBackup deleted the backup, so nothing was left to recover. 2955 then started on the fresh DB (Database migration history diverges … 61:PullRequestReviewGuides (unknown to this build)).
09:46:40: the user retried and the update succeeded, only because the DB was now 4 MB and the backup took seconds.
What we think needs fixing (from reading the launcher in 2908/3042):
restoreDatabaseBackup overwrites the live DB with copyFile without checking for other open handles. Restoring under an open SQLite connection gives an empty or corrupt DB.
#returnToPrevious discards the backup immediately after restoring, even if the restore or the rollback start fails. Keep it until the previous version is confirmed healthy (ideally keep the last N).
backupDatabaseOnce is a raw file copy of a live DB (plus -wal/-shm). The SQLite online backup API or VACUUM INTO would be consistent and is usually faster than copy + fsync of a large file.
The desktop SSH route starts a replacement server while a service-managed server (serviceManaged) is mid-update. It should wait for the service (it can see runtime/service-state.json with update.status = pending).
Backup duration, rollback and backup deletion are not logged, which made this hard to reconstruct.
Mitigations we are putting in place: connect the desktop over HTTPS (pairing link) instead of SSH launch, so it never spawns a server on the host; take our own SQLite online backups outside ~/.t3/runtime; and run updates through a script that pauses the watchdog, backs up first and verifies thread counts afterwards.
Rollback over a live database: if another server owns the home, the trial exits with code 78, and the launcher waits for the owner to leave (#idleWhileStateDirOwned, apps/server/src/serviceLauncher.ts:823). It no longer copies the backup over a database that another process has open.
One mixed-version case is not covered: a server from before the lock whose runtime record another old server already removed. If you hit this again with both sides on a build that has the lock, please reopen with the launcher log.
Activity
JanMolcik commentedon Oct 13, 2026
Same failure here, with a worse outcome: the database came back empty, not malformed. All threads, projects and scheduled tasks were gone. Adding our timeline and the code paths involved, since it pins down the exact race.
Setup: Linux VM,
t3code.service(service launcher from 0.0.46-nightly.20261010.2908), desktop on macOS connected over SSH. Update 0.0.46-nightly.20261011.2955 → 0.0.46-nightly.20261013.3042,statev2.sqliteseveral hundred MB, slow disk (IO pressure "full" 36–64 %).Timeline (local time):
backupDatabaseOncestarts copying the DB file. This took ~5 minutes on the slow disk, and port 3773 was down the whole time.t3 serve --port 3773 --base-dir ~/.t3against the same DB. We run a watchdog that kills foreignt3 serveprocesses on that base dir, so this repeated 5 times (one new server per minute). The first ones served the old, intact DB (their logs show the old threads).listen EADDRINUSE 127.0.0.1:3773(a desktop-launched server holds the port).#handleExit→#returnToPrevious→restoreDatabaseBackuprunscopyFile(backup, statev2.sqlite)over the live file (same inode, truncate + write).effect_sql_migrationsis stamped with that second.discardDatabaseBackupdeleted the backup, so nothing was left to recover. 2955 then started on the fresh DB (Database migration history diverges … 61:PullRequestReviewGuides (unknown to this build)).What we think needs fixing (from reading the launcher in 2908/3042):
restoreDatabaseBackupoverwrites the live DB withcopyFilewithout checking for other open handles. Restoring under an open SQLite connection gives an empty or corrupt DB.#returnToPreviousdiscards the backup immediately after restoring, even if the restore or the rollback start fails. Keep it until the previous version is confirmed healthy (ideally keep the last N).backupDatabaseOnceis a raw file copy of a live DB (plus-wal/-shm). The SQLite online backup API orVACUUM INTOwould be consistent and is usually faster than copy + fsync of a large file.t3 servetakes no exclusive lock on--base-dir, so two servers can own one DB (same root cause as [Bug]: Desktop starts a second backend against the background service database #6097 and [Bug]: Overlapping server lifecycles during a restart leave state.sqlite malformed and make the app unusable #11084; the unmerged fix(server): prevent duplicate servers for one state directory #9652 tried to address it).serviceManaged) is mid-update. It should wait for the service (it can seeruntime/service-state.jsonwithupdate.status = pending).Mitigations we are putting in place: connect the desktop over HTTPS (pairing link) instead of SSH launch, so it never spawns a server on the host; take our own SQLite online backups outside
~/.t3/runtime; and run updates through a script that pauses the watchdog, backs up first and verifies thread counts afterwards.A 1-minute explainer of the failure sequence:
t3-db-wipe-explainer-en.mp4
t3dotgg commentedon Oct 14, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Theo
Closing as fixed by #16102 and #19311, shipped in
0.0.46-nightly.20261014.3105.#idleWhileStateDirOwned,apps/server/src/serviceLauncher.ts:823). It no longer copies the backup over a database that another process has open.One mixed-version case is not covered: a server from before the lock whose runtime record another old server already removed. If you hit this again with both sides on a build that has the lock, please reopen with the launcher log.