Repository navigation
Imported agent sessions duplicate T3-native threads: import dedup never consults the native thread namespace #10933
Description
Activity
Triage
Confirmed on current
main. Import dedup never consults the native thread namespace, so a session T3 already drives can be imported again as a second thread.What the code does
Bulk import in
AgentSessionImporteralways buildsimport:${providerInstanceId}:${providerSessionId}and only asks
getThreadDetailById/getBindingfor that id. A native thread that already stores the same Claude session inresume_cursor_json.resume(or the Codex session inresume_cursor_json.threadId) has a different thread id, so the importer creates a sibling.AgentSessionScanner.alreadyImportedis project-path keyed: it means “a T3 project already exists at this directory,” which matches the contract comment. It does not mean “this provider session is already a live thread.”recentThreadsonly skips prior import-namespace transcripts (getImportedAgentSessionSourcesitself requiresthread_idlikeimport:…) plus in-scan duplicates.Onboarding still imports history into projects with
alreadyImported: true(“existing projects still need their agent history imported”), so the duplicate is created at import time even when the project row already exists.The durable key is already on
provider_session_runtime.ProviderSessionDirectory.listBindings()exists on main and is unused by the bulk importer (tests stub it as unused).Related
- feat: remote parity with ssh + codex resume on the same host #510 (closed) — re-scan dedup by provider thread id inside the import namespace. That path works. This report is native + imported at once. Not a duplicate; do not reopen.
- [Feature]: First-class interop/sync between T3 threads and local Claude Code CLI sessions #6590 (closed) — the Claude import feature this pipeline shipped. This is a gap, not a reopen.
- Bug: Imported Codex tasks use <recommended_plugins> as their titles #10513 (open) — same scanner, different bug (Codex
<recommended_plugins>titles). - feat(web): import individual Claude sessions from a project #10631 (open, conflicting) — per-session Claude attach adds
existingSessionThreads(list bindings +{ resume }) and reuses a native thread on attach/list. It does not change bulkimportRecentAgentThreads, does not change scanneralreadyImported, and the new lookup is Claude-only. Related, not a closer.
No merged fix.
Suggested fix
Before creating an
import:thread, resolve the provider session against all runtime bindings (Clauderesume, CodexthreadId). Skip or attach to the existing native thread. Optionally surface a session-level “already live” flag; do not overload path-keyedalreadyImported.Cross-environment T3 Connect (env B importing the same
~/.claudewhile env A already has native threads) is a separate product question: each environment has its own DB, so a local cursor check on B cannot see A’s threads. The same-environment native +import:clone is the verified bug.Labels:
bug,accepted,via-triage- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 9, 2026 +1, hit this too, thanks for the clear write-up!
Data point from Linux, T3 Code 0.0.45 (server and desktop): a project import from the welcome wizard on 2026-10-01 created 161
import:threads. 153 of them were copies of sessions that native T3 threads had already driven (I matched each import's session id against the native threads' event payloads). 110 of those originals had been archived or deleted, so from the user's side it looked like old archived threads suddenly came back into the legacy sidebar.The copies also replay the raw Claude transcript, so
<task-notification>and Monitor events show up as user messages, which made them look even more broken.Workaround in case it helps anyone: I archived the 153 duplicates by dispatching
thread.archivefor each matched id, leaving the 6 genuinely terminal-started sessions in place. A dedup check against the native namespace (as suggested above) would have prevented all of it.
What happened
A Claude Code session that T3 Code itself drove appears twice in the thread list: once as the native thread that ran it, and once more as an imported thread created from that same session's on-disk transcript.
The two rows have different ids and different generated titles, so they do not look like duplicates in the UI, but they are the same conversation.
Concrete pair, from the machine that still holds its original database (machine A below):
Same underlying Claude Code session. Two threads. 131 sessions are double-represented this way on machine A.
Diagnosis
Two independent places drop the same check, and neither one ever compares a candidate against the native thread namespace.
1. The importer only deduplicates within the import namespace.
apps/server/src/project/AgentSessionImporter.tsbuilds the thread id from the provider session id, then looks for a pre-existing thread using only that import-namespaced id:getThreadDetailByIdis only ever asked aboutimport:claudeAgent:<uuid>. A native thread already driving<uuid>has a completely different thread id, so it is invisible to this lookup. The importer therefore creates a second thread for a session that is already live.2.
alreadyImportedis keyed on project path, not on provider session id.apps/server/src/project/AgentSessionScanner.tsbuilds candidates from the provider CLIs' on-disk transcripts, then sets the flag from a map keyed on workspace root path:The schema comment in
packages/contracts/src/agentSessions.tsagrees with that reading:So the flag answers "does a T3 project already exist at this directory".
AgentSessionScanner.tscontains no occurrence ofthreads,resume_cursor_json, orprovider_session_runtimeat all, so it has no way to consult session identity even in principle. A session already running as a native thread is offered as a fresh import candidate withalreadyImported: false.The durable key needed for both checks is already stored. A native thread's
provider_session_runtime.resume_cursor_jsoncarriesresume(and sometimesresumeSessionAt), which are the Claude Code session uuids naming the exact transcript files the scanner is reading.Why #510 does not cover this. Its acceptance criteria already state the right principle:
That criterion prevents re-importing the same session twice within the import namespace. It does not cover a session existing in the native and imported namespaces simultaneously, which is what this report is about.
Why it got much worse across two environments.
~/.claudewas migrated to a second Mac, so both machines see overlapping transcript sets. Session counts observed on machine A:(Counted one level deep with unique session basenames; a recursive count under
projectsgives a larger number.)Both machines are connected over T3 Connect. The new Mac's scanner offers all 571 as import candidates, including 141 whose sessions are already arriving as native threads from the other environment. Settled counts diverge as a result: 559 on the new machine against 380 on the old one, for what should be the same set of conversations.
Wiping the T3 database and re-onboarding does not help, because the scanner reads
~/.claude/projects, not the database. Every clean onboard re-offers the same set and rebuilds the same overlap.Steps to reproduce
~/.claude/projects/<dir>/<uuid>.jsonl.~/.claudedirectory.alreadyImported: false.Suggested fix. Exclude a transcript from the import candidates, and from the import itself, when a live thread already drives that provider session. Alongside
importedProjectsByRoot, the scanner needs a set of provider session ids taken fromprovider_session_runtime.resume_cursor_jsonfor non-imported threads, andalreadyImported(or a newalreadyLiveflag) would be true when the candidate's session id is in that set. The importer'sgetThreadDetailByIdcheck needs the equivalent native-namespace lookup before it creates a thread. Grouping candidates by directory is still right for the project list; the per-session check is what is missing.Version
Server reports 0.0.40; app bundle is 0.0.41-nightly.20260909.1439. Source citations above were verified against tag
v0.0.40(commit09e8de9).Environment
macOS 26.6 (Darwin 25.6.0), Apple silicon, Node v25.9.0. Two environments connected over T3 Connect. Providers in use: claudeAgent, codex.
Evidence
Related issues
#510 (closed) states the correct dedup principle in its acceptance criteria, but scopes it to re-scans within the import namespace. It does not cover a session that is simultaneously native and imported, so this is not a duplicate and #510 does not need reopening.
Fix applied or workaround
None. No writes were made to the database. Diagnosis was performed against a read-only copy.
Filed by
claude (opus-5) via t3 triage