Repository navigation
[Bug]: Orchestrator V2 nightly silently reuses a preview-created database and misses newer V1 threads #15017
Description
TL;DR: An earlier Orchestrator V2 preview left a September 24 database on disk. The October 3 Orchestrator V2 nightly reused it, so newer stable V1 threads and changes did not appear. A local repair recovered 16 missing threads and 4 projects; detecting this stale preview database or explaining recovery would help other affected users.
Area: apps/server, V1 → Orchestrator V2 migration
Impact: Recent local threads, projects, messages, and thread status changes are silently absent after upgrading to the Orchestrator V2 nightly.
Steps to reproduce
- Run an earlier Orchestrator V2 alpha/preview once. This creates
~/.t3/userdata/statev2.sqlitefrom the then-current V1state.sqlite. - Continue using stable V1, creating threads and changing thread state there. The V1 database advances, while the preview Orchestrator V2 database remains on disk.
- Install and start the first Orchestrator V2 nightly,
0.0.46-nightly.20261003.2610, on the same Mac and data directory. - Inspect the sidebar or compare the local V1 and Orchestrator V2 database inventories. Restarting the nightly does not bring in the V1 changes made since the preview's copy.
Expected behavior
The first public Orchestrator V2 nightly should either migrate the current V1 state, or clearly detect and explain that an older preview-created Orchestrator V2 database is taking precedence and newer V1 data will be omitted. Existing Orchestrator V2-only work must be preserved.
Actual behavior
The nightly silently reuses the September 24 Orchestrator V2 database. Its legacy tables are a snapshot of V1 as of that date, so the lazy importer cannot see later V1 records. Recent threads are missing, and existing threads can retain old snooze/settlement metadata.
Evidence from one affected Mac
- The original
statev2.sqlitewas created September 24, before this Orchestrator V2 nightly. It contained V1 events through sequence 141672. - The current stable V1
state.sqlitecontained events through sequence 149248 on October 2: 7576 later V1 events. - Before repair, the Orchestrator V2 store lacked 16 V1 thread records and 4 projects. 18 existing threads had later V1 messages, and 15 had differing settlement/snooze metadata.
- A backup-based offline reconciliation, followed by restarting the nightly, brought the local store to 0 missing threads, 0 missing projects, 0 pending transcripts, and 0 incorrect non-settled statuses. The recovered threads appeared in the app. This confirms the V1 records were present on disk and the normal Orchestrator V2 startup had not imported them.
The shipped nightly bundle's initializeV2Database returns immediately when statev2.sqlite already exists:
if (yield* fs.exists(destinationPath)) return;The Orchestrator V2 importer then reads the V1 projection tables inside that existing Orchestrator V2 database, not the newer V1 state.sqlite. This explains why restarting cannot catch up.
Scope / possible fix
I understand that the migration guide documents a one-time copy and intentional divergence between Orchestrator V1 and Orchestrator V2. This case is specifically a Orchestrator V2 database seeded by pre-release preview use before the first broadly released Orchestrator V2 nightly, followed by continued stable V1 use. Silently treating that preview database as the release migration source leaves the user with an apparently successful but stale import.
An age/source check with a clear migration choice or recovery guidance would prevent silent loss. Selective catch-up might also work, provided it does not overwrite Orchestrator V2-only data.
The separate behavior of snoozed threads disappearing during ordinary use is not part of this report; the snooze differences above are attributable to this stale migration snapshot.
Environment
- macOS, local T3 Code data store
- T3 Code nightly
0.0.46-nightly.20261003.2610 - Earlier Orchestrator V2 preview/alpha use, followed by continued stable V1 use
Local recovery / attribution
The local workaround was carried out with a Codex agent inside T3 Code: it prepared an offline repair script, backed up both databases, reconciled the missing records, and verified the import after restart. The integrated agent workflow made the investigation and recovery straightforward. This was a local data repair, not an upstream code fix.
AI-assisted investigation and report through Codex in T3 Code; this revision was prepared by GPT-6.1-Sol.
Activity
Paranjayy commentedon Oct 3, 2026
One additional observation from the same upgrade: saved, unsent composer drafts that were visible in the previous version no longer appeared in the Orchestrator V2 nightly.
This is a user-observed symptom so far. We have not checked where those drafts were stored, whether they remain recoverable, or whether the cause is the stale database described above, the desktop profile change, or another migration path. The verified local thread repair did not verify draft recovery.
It may be useful to check draft preservation alongside the V1 → Orchestrator V2 migration. I am adding it here for context; it would need separate storage evidence or a reproduction before treating it as the same confirmed defect.
This comment was prepared by GPT-6.1-Sol through Codex in T3 Code, based on the user's report.
juliusmarminge commentedon Oct 3, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks for such a careful report, @Paranjayy. The before-and-after counts and the offline reconciliation made this straightforward to confirm.
Confirmed on
0.0.46-nightly.20261003.2610. If astatev2.sqlitefile already exists, the nightly treats it as the finished V2 store and never reads the live V1 database again. It can't tell a file left by a preview build apart from one the nightly made itself. That means threads and thread-state changes you made in stable V1 after the preview get skipped without any warning, and restarting won't catch them up. Yourstate.sqliteisn't modified or deleted. The newer V1 rows are still there, they just never get read.initializeV2Databasereturns as soon as the destination file exists (source).projection_threadsfrom inside that V2 file, not fromstate.sqlite(source).How this relates to #14871: copying once is intentional after a real V2 start, and the thread migration guide says later V1 and V2 changes don't sync. This report covers an earlier gap. V2-branch preview builds such as
v0.0.43-preview.20260923.2177already wroteuserdata/statev2.sqliteand had the same early return. The stable nightly from that week didn't use that path at all. So if a preview created that file on September 24, this nightly keeps it and treats it as the migration source.Until there's a fix, and keeping any V2-only work safe:
statev2.sqliteand its-waland-shmfiles somewhere else, but don't delete them yet and don't touchstate.sqlite. Then start the nightly, which will take a fresh copy of your current V1 data. Once your threads look right, you can delete the moved files.Possible fixes: detect a V2 snapshot whose copied V1 tables are older than the current
state.sqliteand warn before continuing, or selectively import the missing V1 threads, messages, and metadata without overwriting V2-only data. Picking between those is a maintainer decision. Adding a note to #14871 alone wouldn't help installs that have already launched this nightly.[-][Bug]: V2 nightly silently reuses a preview-created database and misses newer V1 threads[/-][+][Bug]: Orchestrator V2 nightly silently reuses a preview-created database and misses newer V1 threads[/+]kharitonov-egor commentedon Oct 4, 2026
Hit a variant of this on Windows 11. I never ran a preview build. Here
statev2.sqlitecame from the first public V2 nightly (orchestration_v2_projection_metadataupdated2026-10-03T07:07:42Z).Then I lost my sidebar to #15001 and rolled back to
0.0.45-nightly.20261002.2595to keep working. That puts me in the same trap this issue describes, just without a preview involved. Any V1 work I do now will be missing the next time I start V2, because V2 sees the existingstatev2.sqliteand never readsstate.sqliteagain.The drift has already started. One day after the cutover, V1
state.sqlitehas 428 threads and 12,493 messages, whilestatev2.sqlitehas 422 threads and 12,267 messages.Anyone rolling back to V1 to dodge a V2 bug will hit this. To work around it, I moved
statev2.sqlite*out of~/.t3/userdataso the next V2 launch migrates from current V1 data. It would help if V2 compared V1's last-write time or event sequence with the snapshot it imported from, and re-imported or at least warned when V1 is newer.anenger commentedon Oct 16, 2026
Still reproducible on stable
0.1.0(macOS). The earlier reports here were on nightlies and a manual rollback; this one came through the normal stable release, so the trap is no longer nightly-only.Shape of this instance: a
0.0.46-nightlyran once on this machine, seededstatev2.sqlite, and the machine returned to stable0.0.45about 2.5 minutes later. Stable then wrote V1 for ~37 hours. When0.1.0arrived, it opened the Oct 14 snapshot and six threads were simply gone from the sidebar.Timeline (from
logs/desktop.trace.ndjson.7, local EDT / UTC)desktop.updates.applyUpdateAvailable→0.0.46-nightly.20261014.3105applyUpdateDownloadedOrchestrationV2…ThreadFailureLookupIndex) andstatev2.sqliteseededapplyUpdateAvailable→0.0.45(back to stable)applyUpdateDownloaded0.0.45writes V1 only: events 1000 → 6261, 6 new threads0.1.0first appears in traces, reads the stalestatev2.sqliteDivergence measured on disk
state.sqlite(V1)statev2.sqliteprojection_threadsorchestration_eventsprojection_state.last_applied_sequence2026-10-14T02:28:20.146Z)orchestration_v2_eventsauth_sessionsauth_pairing_linksorchestration_v2_projection_metadatareadslast_sequence = 1151,updated_at = 2026-10-14T11:58:42.752Z— not one V2 write in the ~37 hours between the seed and the0.1.0upgrade.The seed itself was correct — the defect is purely the missing re-sync
Worth isolating, because
999looks like a truncation bug and isn't one. In V1, event 999 occurred at2026-10-14T02:28:20.146Zand event 1000 at2026-10-14T12:31:49.514Z. The seed ran at11:58:41Z, between them, so V1 held exactly 999 events at that instant. Sequences 1–999 instatev2.sqlitematch V1 one-for-one on bothevent_idandsequence(999/999).The copy was therefore complete and correct for the moment it was taken. Everything lost is a consequence of there being no staleness check afterward. Thread-level cut:
2f9e8b79…(seq 2–33),5650d604…(36–999),0d912b99…(751–966)95b9898f…(1000),089fff1c…(1069),57dfed50…(1175),6fdad9c0…(1244),65753d74…(1265),0bf63032…(1404)statev2.sqlitealso carries 152 appended events (seq 1000–1151) whoseevent_ids appear nowhere in V1 (0/152) and which cover only those same 3 threads plus 2 projects — the migration's own re-emittedthread.created/message.updated/turn-item.updatedrecords, ending with twothread.visitedat11:58:42Z.Code is unchanged at the
v0.1.0tagBoth mechanisms @juliusmarminge confirmed are still present:
apps/server/src/persistence/initializeV2Database.ts#L26—if (yield* fs.exists(destinationPath)) return;, guarded on file existence alone. The doc comment states the intent: "Seed V2 once. Its copied legacy tables remain the source for lazy transcript import."LegacyV1ThreadImporter.ts#L575— selectsFROM projection_threadsinside the V2 database, so it can only ever see the snapshot.Suggested detection
Picking up @kharitonov-egor's suggestion, the comparison needs no new bookkeeping — both numbers already exist. At startup, compare V1
projection_state.last_applied_sequence(ormax(sequence)from itsorchestration_events) againstorchestration_v2_projection_metadata.last_sequence. Here that is 6261 vs 1151: a 5110-event gap that could have been surfaced as a prompt or a warning instead of silence. A one-time guard would also help: refuse to seed, or mark the store provisional, when the build writing it is a pre-release and a stable build may follow.Recovery on this machine
The maintainer workaround in this thread worked unchanged. With the app fully quit,
state.sqliteverifiedintegrity_check: okat 9 threads / 6261 events;statev2.sqlite*was moved out ofuserdata/(not deleted) and the app restarted to re-seed from current V1.state.sqlitewas never written to. Re-pairing one device was expected, since 4auth_sessionsrows existed only in the old V2 file.Environment
0.1.0against its local server (serverVersion 0.1.0,orchestrationProtocolVersion 2)0.0.45statev2.sqliteseeded by:0.0.46-nightly.20261014.3105updateChannel: "latest",updateChannelConfiguredByUser: truet30.1.0vianpx t3 triageInvestigated with Claude Opus 5 (
claude-opus-5) in Claude Code, run fromt3 triage. Diagnosis read against av0.1.0tag checkout; counts are from this machine's own databases, with the home directory path redacted.arsotirchellis commentedon Oct 16, 2026
Another stable
0.1.0case (macOS, arm64), with a different starting point: I never rolled back by hand. The switch from Nightly back to the V1 build arrived as an ordinary in-app update.Timeline (from
~/Library/Caches/com.t3tools.t3code.ShipIt/ShipIt_stderr.logand file timestamps, UTC)statev2.sqlitefromstate.sqlite(allorchestration_v2_legacy_imports.shell_imported_at=2026-10-12T18:01:13.615Z;v1-local-storage-importedmarker written at the same time)Moving bundle from T3 Code (Nightly).app to T3 Code (Alpha).app, so the machine is back on the V1 build about 13 minutes laterstate.sqlitefor ~3.5 daysMoving bundle from T3 Code (Alpha).app to T3 Code (Beta).app(0.1.0), which opens the Oct 12 snapshotDivergence measured on disk
state.sqlite(V1)statev2.sqliteprojection_threadsprojection_thread_messagesthread.createdevent. Theircreated_atvalues run from2026-10-12T18:16:53Zto2026-10-16T08:42:09Z. The newest seeded thread was created at2026-10-12T18:02:03Z, so the cut is exact.user/assistant). Theirtranscript_imported_atwas already set on Oct 12, soensureTranscriptnever looks at them again.0.1.0added nothing to those 5 threads, so their gap is pure V1 drift.Same root cause, confirmed in the shipped
0.1.0bundleinitializeV2Databasestill returns whenstatev2.sqliteexists ("Seed V2 once").reconcileShellsandensureTranscriptonly ever read the legacy tables insidestatev2.sqlite.Why this one matters
The issue was opened for preview builds, then manual rollbacks. Here there was no manual downgrade: the app's own updater (ShipIt) swapped the bundle from Nightly to Alpha 13 minutes after the seed, and I carried on working normally. Anyone who briefly ran a V2 Nightly and then moved back to the stable channel before
0.1.0is likely in the same position without knowing it.Local recovery (offline, preserves V2-only data)
Instead of moving
statev2.sqliteaside, which would drop work done in0.1.0since the upgrade, I used the importer's own idempotency:projection_threadsfor the 31 threads;INSERT OR IGNOREofprojection_thread_messages,_pull_requests,_activities,_turns,_proposed_plans,_sessions) fromstate.sqliteintostatev2.sqlite. The table layouts are identical in0.1.0.transcript_imported_at = NULLonorchestration_v2_legacy_importsfor the 5 seeded threads that gained messages.ensureTranscriptskips messages whose import event already exists, so it only adds the missing ones.0.1.0.reconcileShellsimports the 31 threads at startup, and the transcripts fill in.Result after one restart of
0.1.0:thread.createdorchestration_v2_projection_threads0.1.0, untouched)orchestration_v2_legacy_importswithtranscript_imported_at IS NULLlast_erroruser/assistantmessages without amigration:v1:message:<id>eventuser/assistantmessages without a V2 message rowOf the 1,044 legacy message rows copied across, 484 are
user/assistant(322 in the 31 new threads, 162 in the 5 continued ones), and all 484 are now in V2. The other 560 are tool and system rows, which the importer skips by design (role IN ('user', 'assistant')). The most-affected continued thread (300 missing rows) now ends at its last V1 message, a few seconds before the0.1.0update was applied. No restored thread came back archived or deleted.That is effectively the "selective catch-up" from the triage comment, and it takes about 20 lines of SQL. Running it automatically at startup whenever V1's
max(orchestration_events.sequence)is ahead of the snapshot's would close this without touching V2-only data.john3374 commentedon Oct 16, 2026
Same shape on stable 0.1.0, Windows 11, local desktop install. A V2 nightly ran once on 2026-10-03 and seeded
statev2.sqlite, then the machine went back to 0.0.45 (to dodge #15001) and kept writing V1 until 2026-10-15. After the auto-update to 0.1.0, the app opened the Oct 3 snapshot.Before:
state.sqlite53 threads / 27 projects;statev2.sqlite37 threads / 25 projects, allorchestration_v2_legacy_importsrows dated 2026-10-03, no errors, 0orchestration_v2_events(nothing V2-native).Workaround from the triage comment above worked: app quit,
statev2.sqliteplus-waland-shmmoved aside, relaunch. Result: 53 threads, 53 transcripts imported, 0 errors, 27 projects. Took a few seconds.Filed by Claude Fable 5.1 via
t3 triage.immakermatty commentedon Oct 19, 2026
Another way into this, with no channel switching and no manual rollback: an in-app Update server from 0.0.x to 0.1.x that the service launcher rolls back.
prepared. The 30 minutes from fix(server): service updates no longer fail on full disks or large databases #19311 only apply from 0.1.0 on.userdata/statev2.sqlitefromstate.sqlite(Sqlite.layerConfig→initializeV2Database). On a big V1 database the copy alone eats a good part of those 120 s. We measured 33 s for 5.2 GB on an Apple Silicon SSD, and a VM disk is slower.state.sqlite(restoreDatabaseBackuponpending.dbPath, the same in 0.0.45).statev2.sqliteand its-wal/-shmstay, outside the rollback boundary thatdocs/internals/server-updates.mddescribes.state.sqlite. The next update findsstatev2.sqliteand takes it as is, so everything written between the two attempts is missing.Two things that might help pick a fix:
preparednever opened its command gate. As far as we can tell, its copy holds only the seed, the migrations and the import, and no user work. Seeding again whenstate.sqlitechanged after the seed, and the copy has no V2-native writes yet, would cover it without risking V2-only data.This comes from reading the code at
main5b72385 and v0.1.0, not from an end-to-end reproduction. On our hosted machines we movestatev2.sqlite*aside after a rolled-back update.