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]: Worktree pairing can migrate the live desktop database #10516
I searched existing issues and did not find a duplicate of this specific failure.
I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Keep the installed desktop server running.
In a development worktree with a pending migration, run node apps/server/src/bin.ts pair without --base-dir, with no discoverable worktree server.
Discovery falls back to the installed server; token creation opens its database using the development checkout's migrations.
Expected behavior
Worktree pairing must not silently target the live install. Minting a token should not run a different checkout's schema migrations.
Actual behavior
An agent trying to pair a dev browser migrated my live database. On September 3, the command returned the installed server on port 3773. The live DB recorded migration 48 (ProjectionThreadMessageContext, unmerged work) and the t3 pair token in the same second: 16:33:46 UTC.
Updating later broke both installed builds: released migration 48 is ProjectionThreadBranchPullRequest, so it was skipped and startup failed on the missing column.
Related: #8896 covers migration identity validation; this report concerns the pairing command that unexpectedly applies development migrations to live data.
Impact
Blocks work completely
Version or commit
Affected installed builds: 0.0.39 and 0.0.40-nightly.20260907.1346; pairing ran from an unmerged inline-context worktree.
Environment
macOS, Electron desktop; Node CLI invoked from a linked git worktree.
Logs or stack traces
Pairing with MacBook Pro (http://127.0.0.1:3773).
PersistenceSqlError: ProjectionSnapshotQuery.getCommandReadModel:listThreads:query
Error: no such column: branch_pull_request_json
Confirmed on current main. This is a real, data-destructive pairing bug, not a duplicate of #8896.
What we verified
t3 pair without --base-dir discovers a live server, then mints a token by opening that server’s SQLite file in-process.
Discovery falls through to the install. In apps/server/src/cli/pair.ts, discoverPairTarget checks the worktree .t3 first, then always appends the shared T3 home (T3CODE_HOME / ~/.t3). The first live server-runtime.json + pid + /.well-known/t3/environment hit wins. If no worktree server is running, that is the installed desktop (typically port 3773).
The file comment says this matches dev-runner precedence. It does not. scripts/dev-runner.ts is exclusive (worktreeHome ?? shared home). Pairing is inclusive and silently retargets the daily-driver DB.
Minting applies this checkout’s migrations.mintPairingLink provides EnvironmentAuth.runtimeLayer, which includes SqlitePersistenceLayer. apps/server/src/persistence/Layers/Sqlite.ts calls runMigrations() on every open. Those migrations come from the CLI checkout, not from the running install.
That matches the report: worktree pair → “Pairing with MacBook Pro (http://127.0.0.1:3773)” → live journal wrote unmerged 48_ProjectionThreadMessageContext in the same second as the t3 pair token. Released 048 is ProjectionThreadBranchPullRequest (branch_pull_request_json). Effect’s migrator then skipped it by ID, and both 0.0.39 and 0.0.40-nightly.20260907.1346 failed on the missing column.
apps/server/src/cli/pair.test.ts covers explicit --base-dir success/failure and stale pid/state. It does not cover “linked worktree, no local server, shared home must not be used.”
Fail closed in a worktree. If resolveWorktreeT3Home(cwd) is set and that home has no live server, raise NoRunningServerError. Only target the shared install when --base-dir is explicit (or the cwd is not a linked worktree). Add a regression test for that fall-through.
Do not migrate a live install from another checkout. Pairing should ask the already-running server to mint the token, or refuse if effect_sql_migrations names do not match this checkout. Opening the install DB through runtimeLayer is unsafe even if discovery is later made explicit.
migrate-dev-db.ts already refuses slot collisions. Pairing should be at least that careful, and should not touch the install DB from a worktree by default.
Workaround / recovery
Pair a worktree only with t3 pair --base-dir <worktree>/.t3 after that worktree’s server is up.
Do not run pair / auth pairing create from a feature checkout against ~/.t3.
Implement the fail-closed discovery change and the regression first; that stops the live-DB write. Treat “mint without this checkout’s runMigrations()” as the follow-up so auth pairing create cannot do the same thing with an explicit --base-dir.
Activity
juliusmarminge commentedon Sep 7, 2026
Triage
Confirmed on current
main. This is a real, data-destructive pairing bug, not a duplicate of #8896.What we verified
t3 pairwithout--base-dirdiscovers a live server, then mints a token by opening that server’s SQLite file in-process.Discovery falls through to the install. In
apps/server/src/cli/pair.ts,discoverPairTargetchecks the worktree.t3first, then always appends the shared T3 home (T3CODE_HOME/~/.t3). The first liveserver-runtime.json+ pid +/.well-known/t3/environmenthit wins. If no worktree server is running, that is the installed desktop (typically port 3773).The file comment says this matches dev-runner precedence. It does not.
scripts/dev-runner.tsis exclusive (worktreeHome ?? shared home). Pairing is inclusive and silently retargets the daily-driver DB.Minting applies this checkout’s migrations.
mintPairingLinkprovidesEnvironmentAuth.runtimeLayer, which includesSqlitePersistenceLayer.apps/server/src/persistence/Layers/Sqlite.tscallsrunMigrations()on every open. Those migrations come from the CLI checkout, not from the running install.That matches the report: worktree pair → “Pairing with MacBook Pro (http://127.0.0.1:3773)” → live journal wrote unmerged
48_ProjectionThreadMessageContextin the same second as thet3 pairtoken. Released048isProjectionThreadBranchPullRequest(branch_pull_request_json). Effect’s migrator then skipped it by ID, and both0.0.39and0.0.40-nightly.20260907.1346failed on the missing column.apps/server/src/cli/pair.test.tscovers explicit--base-dirsuccess/failure and stale pid/state. It does not cover “linked worktree, no local server, shared home must not be used.”Related, not duplicates
~/.t3/userdata.Suggested fix
resolveWorktreeT3Home(cwd)is set and that home has no live server, raiseNoRunningServerError. Only target the shared install when--base-diris explicit (or the cwd is not a linked worktree). Add a regression test for that fall-through.effect_sql_migrationsnames do not match this checkout. Opening the install DB throughruntimeLayeris unsafe even if discovery is later made explicit.migrate-dev-db.tsalready refuses slot collisions. Pairing should be at least that careful, and should not touch the install DB from a worktree by default.Workaround / recovery
t3 pair --base-dir <worktree>/.t3after that worktree’s server is up.pair/auth pairing createfrom a feature checkout against~/.t3.state.sqlitefrom backup, or repair the journal/schema after a snapshot (same class of recovery as Colliding migration IDs silently skip migrations; one undecodable event row bricks startup #8896). Reinstalling the app will not rewrite the ledger.Next step
Implement the fail-closed discovery change and the regression first; that stops the live-DB write. Treat “mint without this checkout’s
runMigrations()” as the follow-up soauth pairing createcannot do the same thing with an explicit--base-dir.