Skip to content

fix(server): CLI commands no longer migrate the database of a running server - #26763

Merged
juliusmarminge merged 8 commits into
mainfrom
fix/issue-26696
Nov 11, 2026
Merged

juliusmarminge merged 8 commits into
mainfrom
fix/issue-26696

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

Problem

CLI commands that open the T3 home's database (t3 auth …, t3 pair, t3 thread …, t3 connect …, t3 project …, t3 environment …) ran their own build's pending migrations even while a running server owned that home. A newer CLI next to an older server moved the server's schema under it. A later server build that ships a different migration with the same id then refused to start ("would skip required schema migrations").

Reproduced on main 5b89a53:

  1. Start a server on a scratch home.
  2. Add a stand-in migration 073_CliOnlyProbe to the checkout.
  3. Run t3 auth session issue, t3 pair --ttl 5m or t3 environment list with --base-dir set to that home.

Each command exited 0 and the ledger moved from 72 to 73 CliOnlyProbe. The server was still running and still held server-owner.sqlite.

Change

  • CLI commands open the database through a new SqlitePersistence.layerCliConfig. The server and the offline t3 project path, which already hold ownership, keep layerConfig.
  • Before migrating, the CLI layer checks whether this build has migrations the database has not recorded (hasPendingMigrations, the same rule the Migrator and the V2-preview reconciliation follow).
    • Nothing pending: it runs the usual read-only path. It takes no lock, and history checks still run.
    • Pending: it holds the T3 home's ownership lock only while migrating. If another process, usually a running server, holds that lock, the command fails with ServerOwnsDatabaseError. The error says to use the t3 that matches the running server or stop it, and the database is left unchanged.
  • serverOwnership.ts gains holdServerOwnershipLock, the lock alone, without the runtime-state claim. acquireServerOwnership now shares the same lock code.
  • isWildcardHost and formatHostForUrl move from startupAccess.ts to @t3tools/shared/hostClassification. Without the move, Sqlite.ts importing serverOwnership.ts creates an import cycle back through EnvironmentAuth.ts. Only imports change.

Scope and approval

Fixes #26696

Verification

  • EnvironmentAuth.layer > does not migrate a T3 home behind the server that owns it, in apps/server/src/auth/EnvironmentAuth.test.ts. It holds the ownership lock, seeds a database one migration behind, and builds the CLI storage layer. It fails on main with expected 72 to be 71, and passes here.
  • opens a current T3 home while the server that owns it runs, in the same file. With nothing pending, the CLI still opens the database beside an owner.
  • apps/server: EnvironmentAuth, Sqlite, Migrations, pair and project tests pass, 51 of 51. tsc --noEmit is clean. vp run check:changed passes.
  • Runtime transcript, from a scratch home and a server started from the checkout. A newer CLI now exits 1 with ServerOwnsDatabaseError and the ledger stays at 72. The unmodified server and a build with a different 073 both start afterwards; on main the latter refused. Control: with nothing pending, auth session issue and pair exit 0 beside the running server. transcript-26696.md, driver: repro.sh
  • Server-only change with no visible surface, so there are no captures.
  • Not checked:
    • Published binaries (a stable server with a nightly npx t3). The stand-in migration exercises the same path.
    • Servers older than 0.1.0, which have no ownership lock. The lock-only check does not detect them.

Claude Opus 5.5 (Claude Code), with step agents on Claude Opus 5.5 and GPT 6.1 Sol (Codex), through T3 Code.

🤖 Generated with Claude Code

juliusmarminge and others added 4 commits October 28, 2026 22:59
…#26696)

The test holds the T3 home's ownership lock and seeds the database one
migration behind the build. It then builds EnvironmentAuth.layerRuntime,
the storage layer that t3 auth, pair, connect, thread and project share.
On main the layer applies the pending migration behind the owner. The test
expects the layer to fail with a "server owns" error and the ledger to stay
where it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er owns

`t3 auth`, `pair`, `connect`, `thread`, `project` and `environment` open
the T3 home's database through the server's persistence layer, which
applies this build's pending migrations unconditionally. A newer CLI
next to an older running server therefore rewrote the schema that server
runs on, and the next older build refused to start.

CLI commands now use `SqlitePersistence.layerCliConfig`. When this build
has migrations to run (no ledger, an id above the highest recorded, or
the V2-preview reconciliation), it holds the T3 home's ownership lock
for the migration and fails with `ServerOwnsDatabaseError` if a running
server holds it. With nothing pending it takes no lock, so a starting
server is never refused. The server and the offline `t3 project` runtime,
which already own the home, keep the plain layer.

`holdServerOwnershipLock` is the lock half of `acquireServerOwnership`,
without the runtime-state claim and pre-lock heuristics. Importing it
from persistence closed a module cycle through `serverRuntimeState`'s
use of `startupAccess`, so the two host-string helpers it needed move to
`@t3tools/shared/hostClassification`.

Fixes #26696

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: with nothing pending, run the read-only migration path so
divergent histories are still reported, and name any T3 Code process as
the possible lock holder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 29, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 111f112

Macroscope's review found this PR approvable — CLI commands now require database ownership before applying pending migrations, preventing schema changes behind a running server while preserving current-schema access. Existing server migration behavior remains unchanged, and the reported runtime checks cover both refusal and normal-access paths.

You can add or adjust custom eligibility rules. Learn more.

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: f7b81e1 · PR result: 111f112 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

CLI persistence now checks migration history and uses a server ownership lock before applying pending migrations. Host-formatting and wildcard-detection helpers now come from the shared host-classification module.

Changes: CLI migration ownership guard

Layer / File(s) Summary
Acquire and release the ownership lock
apps/server/src/serverOwnership.ts
A shared helper acquires the ownership lock and maps SQLite busy errors to ServerAlreadyRunningError. holdServerOwnershipLock holds the lock for a scope. acquireServerOwnership uses the helper and retains owner-matched runtime-state cleanup.
Select migration behavior for persistence layers
apps/server/src/persistence/Migrations.ts, apps/server/src/persistence/Sqlite.ts
hasPendingMigrations checks for a missing history table, a recorded ID below the latest migration, or an OrchestrationV2 row at ID 53 or 54. SQLite setup now accepts a migration effect. The CLI configuration acquires the ownership lock before applying pending migrations and reports ServerOwnsDatabaseError if another server holds it.
Wire CLI persistence and verify ownership behavior
apps/server/src/auth/EnvironmentAuth.ts, apps/server/src/cli/environment.ts, apps/server/src/cli/project.ts, apps/server/src/cli/deferredImports.test.ts, apps/server/src/persistence/Sqlite.test.ts
Auth storage and CLI environment layers use layerCliConfig. A comment records that the project runtime layer is built after ownership is acquired. SQLite tests cover an owned database with pending migrations and one with current migrations. The deferred-import test no longer asserts that the ownership file is absent.

Shared host classification helpers

Layer / File(s) Summary
Define and use shared host helpers
packages/shared/src/hostClassification.ts, apps/server/src/startupAccess.ts, apps/server/src/serverRuntimeState.ts, apps/server/src/serverRuntimeStartup.ts, apps/server/src/cli/pair.ts, apps/server/src/environment/DirectEndpoints.ts
The shared module exports wildcard-host detection and URL host formatting. Server consumers import these helpers from the shared module, and startupAccess.ts removes its local implementations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

Suggested reviewers: maria-rcks, t3dotgg

Merge Risk: Minimal · up to 111f1

CLI commands now refuse to migrate a database owned by a running server when migrations are pending. No concrete merge-blocking risk was found in this change.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title is concise, specific, and accurately describes the primary change: CLI commands no longer migrate a database owned by a running server.
Description check Passed The description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the reproduction, implementation, linked issue, test results, runtime validation, and …
Linked Issues check Passed Issue #26696 requires CLI persistence commands to avoid applying pending migrations to a T3 home owned by a running server. SqlitePersistence.layerCliConfig uses hasPendingMigrations and acquires …
Out of Scope Changes check Passed The changes stay within issue #26696. serverOwnership.ts extracts shared lock acquisition so the CLI can hold the existing ownership lock without claiming runtime state. Migrations.ts adds the pen…
Full details: Description check

Explanation

The description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the reproduction, implementation, linked issue, test results, runtime validation, and unverified cases.

Full details: Linked Issues check

Explanation

Issue #26696 requires CLI persistence commands to avoid applying pending migrations to a T3 home owned by a running server. SqlitePersistence.layerCliConfig uses hasPendingMigrations and acquires holdServerOwnershipLock before migration. A busy lock produces ServerOwnsDatabaseError; the error states that the database was not changed. The no-pending path validates history without taking the lock. CLI consumers use this layer, while the offline t3 project path retains layerConfig because it already owns the home. The added SQLite tests cover both the blocked pending-migration case and the current-database case. The shared host-helper move supports the import-cycle fix and does not alter the issue behavior.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue #26696. serverOwnership.ts extracts shared lock acquisition so the CLI can hold the existing ownership lock without claiming runtime state. Migrations.ts adds the pending-migration check required by the new CLI path. Host-helper relocation removes an import cycle caused by the ownership dependency, and the related import-only updates preserve existing behavior. The added tests verify the requested migration and ownership behavior.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 866a3d9 and 111f112.

Files selected for processing: 11

  • apps/server/src/auth/EnvironmentAuth.ts
  • apps/server/src/cli/deferredImports.test.ts
  • apps/server/src/cli/environment.ts
  • apps/server/src/cli/pair.ts
  • apps/server/src/cli/project.ts
  • apps/server/src/environment/DirectEndpoints.ts
  • apps/server/src/persistence/Migrations.ts
  • apps/server/src/persistence/Sqlite.test.ts
  • apps/server/src/persistence/Sqlite.ts
  • apps/server/src/serverOwnership.ts
  • apps/server/src/serverRuntimeStartup.ts
Files with no reviewable changes: 2

  • apps/server/src/cli/deferredImports.test.ts
  • apps/server/src/cli/pair.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

# Conflicts:
#	apps/server/src/auth/EnvironmentAuth.test.ts
#	apps/server/src/cli/environment.ts
#	apps/server/src/environment/DirectEndpoints.ts
#	apps/server/src/persistence/Sqlite.ts
#	apps/server/src/serverOwnership.ts
The project CLI added on main opened the database through the server's
persistence layer, so it could still migrate a home a running server owns.
Move the CLI ownership tests to Sqlite.test.ts against layerCliConfig
itself, each with its own T3 home.

AI-assisted; verified with the tests and tools listed above.

Copy link
Copy Markdown
Member Author

The automated pipeline stopped on this pull request and will not merge it:

someone else pushed to fix/issue-26696 (head is now 866a3d9); the pipeline does not build on commits it did not review

It stays open for a person to finish or close.

The offline project runtime is built after the command acquires the
server ownership lock, so layerCliConfig would contend with that same
lock if a migration were pending. Use the plain layer there again.

The deferred-imports test no longer asserts that server-owner.sqlite is
absent: the CLI's migration lock now creates it on a fresh home. The
runtime-state assertion still proves nothing was cleared.

AI-assisted; verified with the tests and tools listed above.
@macroscopeapp
macroscopeapp Bot dismissed their stale review November 11, 2026 03:51

Dismissing prior approval to re-evaluate 111f112

@juliusmarminge

Copy link
Copy Markdown
Member Author

Evidence

Started a real server on a fresh disposable home on each commit. Matching-manifest session issuance and pairing succeeded on both. Appended harmless CLI-only migration 81, then ran session issuance, pairing, and environment listing against the running server. Finally stopped the server and attempted startup with a conflicting migration 81.

Before After
f7b81e1d11 111f11209c
Before: CLI silently advances the running server database After: CLI refuses migrations and preserves the database
Session issuance records migration 81 and creates the probe table. Conflicting server startup fails. All three commands return ServerOwnsDatabaseError; schema and ledger remain unchanged. Subsequent server startup succeeds.
Full transcript · Transcript playback · Schema/ledger snapshots Full transcript · Transcript playback · Schema/ledger snapshots

Environment: Linux, real source CLI and HTTP server, fresh synthetic homes. Images and videos render captured transcripts; they are not screen recordings. Credential output was withheld.

Reproduction driver. Evidence uploaded using the ghdrop skill.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Pairing and environment listing were not independently tested as the first pending-migration command on base: session issuance had already applied migration 81. On head, migration 81 remained pending for all three commands.
  • No live terminal screen recording was captured; supplied videos are readable presentations of captured real command output.

@juliusmarminge
juliusmarminge merged commit 5da53f8 into main Nov 11, 2026
34 checks passed
@juliusmarminge
juliusmarminge deleted the fix/issue-26696 branch November 11, 2026 11:55
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Nov 11, 2026
**v0.1.10-nightly.20261111.4529** · 54 changes from 29 contributors

- **Find in browser.** Search pages in the desktop integrated browser with Cmd+F or Ctrl+F.
- **GitLab overview threads.** Resolve merge request overview threads from Summary and Timeline.
- **Faster worktree updates.** Checkpoint captures and status refreshes avoid rereading every file in newly created worktrees.
- **Imported session history.** Imported agent sessions now show their existing history instead of opening as empty threads.
- **SSH reconnect prompts.** Expired SSH verification prompts no longer stack up and force an app restart.
- **Large reasoning traces.** Opening a thread with a large collapsed reasoning trace no longer freezes the app.
- **Terminal scrollback.** Terminal history retains substantially more output instead of silently discarding older lines.

### Plus 45 more bug fixes and performance improvements.

<details>
<summary>Full changelog</summary>

## What's Changed
* fix(server): new worktrees refresh their index once, so captures and status stop re-hashing every file by @josephv123 in pingdotgg/t3code#21341
* fix(usage): Grok and Cursor usage limits show pace and the even-spend line by @wookiist in pingdotgg/t3code#21897
* fix(web): retain diff selection across navigation by @aleo113 in pingdotgg/t3code#22347
* fix(mobile): subscription widget no longer expires on a window that already reset by @juliusmarminge in pingdotgg/t3code#22993
* fix(opencode): verify CLI updates when loopback server is still running by @intrepid37 in pingdotgg/t3code#22839
* fix(usage): Claude subagent output is no longer undercounted by @rfrost-xyz in pingdotgg/t3code#20276
* fix(server): a remote update points the service unit at the runtime it committed by @Marve10s in pingdotgg/t3code#19652
* fix(server): a limited provider's queue stays held after another provider's turn succeeds by @Marve10s in pingdotgg/t3code#22425
* fix(shared): keep spreadsheet shorthand as inline code by @juliusmarminge in pingdotgg/t3code#23203
* fix(web): offer retry while an environment reconnects by @juliusmarminge in pingdotgg/t3code#23194
* fix(server): custom Codex providers no longer report native quota by @fixfon in pingdotgg/t3code#18622
* fix(desktop): Cmd+F now searches the page in the integrated browser by @armanarutiunov in pingdotgg/t3code#22960
* fix(desktop): an SSH password prompt that ended leaves the dialog instead of stacking by @Umais-Adeed in pingdotgg/t3code#34192
* fix(web): render Claude insight blocks as a callout by @macodev00 in pingdotgg/t3code#22239
* fix(scheduler): show short server timezone on fixed-time labels by @shawk08033 in pingdotgg/t3code#23111
* fix(web): focus the primary action when confirmation dialogs open by @rgrvlsk in pingdotgg/t3code#18187
* fix(web): Changes row matches the diff comparison by @whoisaldo in pingdotgg/t3code#19553
* fix(server): imported ACP sessions show their native history by @only21mil in pingdotgg/t3code#22155
* fix(preview): always return evaluated results by value by @juliusmarminge in pingdotgg/t3code#22948
* fix(provider-core): a model reply of "title" no longer replaces the thread title by @juliusmarminge in pingdotgg/t3code#25743
* fix(desktop,web): Open in runs locally for a background service sharing the desktop's T3 home by @KinanLak in pingdotgg/t3code#21661
* fix(web): question answers submit while normal sends are blocked by @t3dotgg in pingdotgg/t3code#22874
* fix(server): cancelling a background-only delegated task reports it interrupted by @only21mil in pingdotgg/t3code#21676
* fix(web): fall back to a readable server for shared repositories by @sanjaibalajee in pingdotgg/t3code#21530
* fix(web): composer follows an optionless thread instead of the settings default by @f4llenz in pingdotgg/t3code#16546
* fix(web): don't save retention age until committed by @macodev00 in pingdotgg/t3code#22208
* fix(server): one dropped probe no longer lists a live linked peer as unreachable by @juliusmarminge in pingdotgg/t3code#34208
* fix(server): preview automation reports ambiguous selectors instead of calling them invalid by @juliusmarminge in pingdotgg/t3code#34205
* fix(opencode): a context overflow that OpenCode compacts no longer fails the turn by @juliusmarminge in pingdotgg/t3code#26743
* fix(web): Ctrl + "+" zooms on keyboard layouts with a dedicated plus key by @juliusmarminge in pingdotgg/t3code#26408
* fix(ci): release version bump push retries when main moves by @t3dotgg in pingdotgg/t3code#34326
* fix(server): CLI commands no longer migrate the database of a running server by @juliusmarminge in pingdotgg/t3code#26763
* fix(web): keep saved default model when changing effort by @macodev00 in pingdotgg/t3code#22193
* fix(web): reveal expanded work rows hidden behind the composer by @ln-bodicherla in pingdotgg/t3code#21968
* fix(composer): steer instead of queueing during automatic compaction by @NikitaMGrimm in pingdotgg/t3code#24138
* feat(gitlab): resolve overview threads from Summary and Timeline by @TonybynMp4 in pingdotgg/t3code#30575
* fix(web): pull request search keeps partial-word matches after GitHub answers by @juliusmarminge in pingdotgg/t3code#26992
* fix(server): Claude subagents no longer show the parent's effort by @juansuaya17 in pingdotgg/t3code#17042
* fix(server): runs cancelled by a server restart say why in the thread by @juliusmarminge in pingdotgg/t3code#27192
* fix(client-runtime): server updates stop waiting 32 minutes when the host returns without this update's outcome by @juliusmarminge in pingdotgg/t3code#27275
* fix(web): Stop shows a pending state and repeat clicks no longer resume by @voida6 in pingdotgg/t3code#20809
* fix(web): collapsed reasoning previews no longer process the whole block by @juliusmarminge in pingdotgg/t3code#23001
* fix(server): storage cleanup skips stale worktree paths under a regular file by @juliusmarminge in pingdotgg/t3code#26594
* fix(terminal): use a byte budget for Ghostty scrollback by @zero-two-rafaeltab in pingdotgg/t3code#8766
* fix(server): storage cleanup keeps worktrees whose file listing exceeds the inspection limit by @juliusmarminge in pingdotgg/t3code#26593
* fix(server): delete temporary ACP health probe sessions by @juliusmarminge in pingdotgg/t3code#25397
* perf(client): streamed text no longer republishes thread queue and checkpoints by @juliusmarminge in pingdotgg/t3code#27425
* fix(web): convert composer list markers completed later by @juliusmarminge in pingdotgg/t3code#22997
* test: remove tests that repeat another case or restate the source by @shivamhwp in pingdotgg/t3code#34170
* fix(server): drop stale queued pull-request watch wakes by @Sinan53x in pingdotgg/t3code#21239
* fix(web): preview recordings stay decodable after a viewport resize by @MansTomb in pingdotgg/t3code#10495
* fix(server): only offer claude update for native Claude installs by @AI091 in pingdotgg/t3code#14170
* fix(server): tell outside agents which MCP tools need a thread and that t3_thread_list takes allProjects by @juliusmarminge in pingdotgg/t3code#26525

## New Contributors
* @wookiist made their first contribution in pingdotgg/t3code#21897
* @aleo113 made their first contribution in pingdotgg/t3code#22347
* @intrepid37 made their first contribution in pingdotgg/t3code#22839
* @rfrost-xyz made their first contribution in pingdotgg/t3code#20276
* @armanarutiunov made their first contribution in pingdotgg/t3code#22960
* @KinanLak made their first contribution in pingdotgg/t3code#21661
* @sanjaibalajee made their first contribution in pingdotgg/t3code#21530
* @ln-bodicherla made their first contribution in pingdotgg/t3code#21968
* @juansuaya17 made their first contribution in pingdotgg/t3code#17042
* @zero-two-rafaeltab made their first contribution in pingdotgg/t3code#8766
* @MansTomb made their first contribution in pingdotgg/t3code#10495
* @AI091 made their first contribution in pingdotgg/t3code#14170

**Full Changelog**: pingdotgg/t3code@v0.1.9-nightly.20261111.4515...v0.1.10-nightly.20261111.4529

</details>


Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.1.10-nightly.20261111.4529
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:needs-julius Needs Julius: route gate, unverifiable surface, or blocked size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: t3 auth, t3 pair, t3 thread and other CLI commands apply the CLI's own migrations to a running server's database

1 participant