Skip to content

Two-way teleport with native CLI sessions - #8357

Closed
baanish wants to merge 133 commits into
pingdotgg:mainfrom
baanish:cursor/teleport-7136-mirror-4ce4
Closed

Two-way teleport with native CLI sessions#8357
baanish wants to merge 133 commits into
pingdotgg:mainfrom
baanish:cursor/teleport-7136-mirror-4ce4

Conversation

@baanish

@baanish baanish commented Aug 27, 2026

Copy link
Copy Markdown

What this adds

T3 can now list, import, and export native Codex and Claude CLI sessions — "teleport." Start a session in either CLI and pick it up in T3, or hand a T3 thread back to the CLI, without losing the transcript and without two writers ever touching the same session file.

Three ideas hold the design together:

Native files stay native. Import copies a CLI session onto a T3 thread; export writes the thread back out in the CLI's own session format. T3 never becomes the owner of the native file.

One writer at a time. Every thread tracks a presence — t3 or native — that says which side currently owns the session. The UI swaps Import/Export based on presence, so both sides can never be writers at once.

Divergence is detected, never merged. After a successful import, a SHA-256 revision watch tracks the native file. If the CLI file changes or goes missing, further T3 turns are blocked and a conflict banner offers Fork native changes, which copies the diverged transcript into a new T3 thread. Both copies survive; nothing gets clobbered.

Scope note: TeleportProvider is codex | claudeAgent only. Grok and OpenCode remain regular T3 providers — this PR adds no native CLI teleport for them.

Fail-closed behavior

Every path that could produce a second writer refuses instead of guessing:

  • File lock. Import and export refuse while the CLI holds the session file. Lock detection uses lsof; if lsof can't be spawned, Unix falls back to an exclusive-open probe and fails closed whenever the file exists. A missing file counts as unlocked, so a first export can still create it.
  • Composer. Send, prompt, and stash are locked while presence is native or importing. The message-loading and attachment-upload disables stack on top of that lock — they don't replace it.
  • Turn ingress. Every thread.turn.start, over WebSocket or HTTP (/api/orchestration/dispatch), requires a matching native revision. The one exception is bootstrap.createThread when the thread doesn't exist yet.
  • Conflict banner. Once the watched revision diverges, send stays blocked until the user forks.

Recovery

In-place import is a single orchestration transition (thread.teleport.import). A failed or interrupted import reverts presence: importing and stays retryable — including first-time directory-bound threads, so a failed first import never looks like it succeeded. Startup recovery deletes leftover new-thread husks and clears or restores existing threads. Custom instance homes, worktree threads, and archived canonical threads are all supported.

Projection adds a teleport_json column (migration 044) and backfills only valid TeleportProvider values.

Known limitations

There is no cross-store transaction spanning orchestration, the provider directory, and the native file — this is documented, not hidden. Mobile already blocks send while a thread is teleported out; the teleport-in UI is web/desktop only for now.

Why

Work started in the Codex or Claude CLI should be able to continue in T3, and the reverse, without losing the transcript or writing from both sides at once. Presence enforces a single writer, the file lock and revision watch back it up, and forking recovers cleanly when the CLI file changes after import.

Relates to #207, #5146, #5741, and #6590. Supersedes #7136.

UI changes

Import lives on the new-thread / sessions view. Export lives in the chat header. The composer is disabled while a thread is in the native CLI.

Entry points

Draft hero Import a native CLI session link
Command palette Import sessions action
Native Claude and Codex sessions in the import picker

Tooltips

Teleport Out tooltip
Teleport In tooltip

Composer disabled after teleport out

Composer disabled when the thread is in the native CLI

Native-revision conflict banner

Shown when the revision watch detects the CLI file diverged after import. Send is blocked until the user forks.

Native CLI session changed banner in the dark chat UI
Fork native changes native-revision conflict banner

Recordings

Codex — full roundtrip: T3 export → native CLI (dark mode) → import back

Codex teleport roundtrip

Claude — full roundtrip: T3 export → native CLI (dark mode) → import back

Claude teleport roundtrip

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

High Risk
Touches turn admission, history replacement, attachment deletion, and native revision gating—mistakes could block sends, lose messages/attachments, or allow divergent native/T3 writers.

Overview
Adds teleport as a first-class thread concern: threads carry TeleportThreadState (native vs T3 vs importing), new orchestration commands/events (thread.teleport.set, thread.teleport.import, thread.teleport.clear, thread.history.replace), and decider rules that block turns on archived, native, or importing presence while allowing atomic import (unarchive + presence + history replace).

Server persistence and projections gain teleport_json (migration 044 with Codex/Claude backfill), hydrate teleport on shells/details (including archived threads for import/UI), and handle thread.history-replaced by wiping turns, plans, activities, and pending approvals while pruning attachment files via an owned-vs-kept plan so unrelated threads are not deleted.

Ingress and runtime: HTTP/WS dispatch runs TeleportService.requireNativeRevisionForTurn before thread.turn.start (skipped for bootstrap create on a missing thread); teleport RPCs get auth scopes; environment advertises capabilities.teleport. Provider changes treat turn.aborted like completion for session state and stop recovering stale sessions on interruptTurn.

Mobile mirrors web fail-closed behavior: the thread composer disables send and shows teleportSendDisabledReason when the thread is teleported out.

Reviewed by Cursor Bugbot for commit ca3e9b7. Configure here.

Note

Add two-way teleport with native CLI sessions (Codex/Claude)

  • Introduces a TeleportService that lists, imports, and exports native CLI sessions (Codex JSONL, Claude JSONL) with format-specific adapters, file locking, bounded reads, atomic writes, and symlink-safe directory traversal.
  • Adds orchestration commands thread.history.replace, thread.teleport.set, thread.teleport.import, and thread.teleport.clear, plus corresponding events thread.history-replaced and thread.teleported; the decider blocks turn starts on native/importing presence and can unarchive threads on import.
  • Persists teleport state via a new teleport_json column on projection_threads (migration 44) and hydrates it through projection handlers, snapshot queries, and client-side thread reducers.
  • Web UI adds a command-palette import flow, a TeleportOutButton in the chat header, native-conflict banners with fork actions, and composer locking (text, attachments, annotations, review comments) when a thread is teleported out or importing. Mobile disables sending for teleported-out threads.
  • interruptTurn in makeProviderService now passes allowRecovery: false, preventing stale-session recovery before an interrupt.
  • Behavioral Change: thread.turn.start may now fail early with a native-revision check via turnStartRequiresNativeRevisionCheck and TeleportService.requireNativeRevisionForTurn before dispatch in both HTTP and WS paths; archived threads are returned by by-id snapshot queries (list queries still exclude them).

Macroscope summarized a6b7b47.

cursoragent and others added 30 commits August 15, 2026 08:54
Contracts, orchestration, and the teleport service land first. Native CLI
formats register later, so list/import/export stay empty until a provider
adapter is added.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
The UI can list, import, and export native sessions. Until a format
adapter is registered, the picker stays empty and export fails closed.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Register the Codex jsonl adapter so T3 can list, import, and export
rollouts the CLI can resume.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Register the OpenCode adapter so T3 can list, import, and export text
turns from opencode.db or JSON storage without treating the live db as a
foreign lock.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Register the Claude jsonl adapter so T3 can list, import, and export
sessions from the Claude projects folder.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Register the Grok session-directory adapter so T3 can list, import, and
export native Grok Build chats.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Stop marked the turn interrupted but left the session running, so the thread stayed Working after abort hung or only emitted turn.aborted.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
A projected running OpenCode thread stayed Working after Stop if the
in-memory provider session was already gone. Interrupt no longer
recovers a session just to abort it, and Stop settles the projection
when there is nothing live to interrupt.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep personal project directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep personal project directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep personal project directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep personal project directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep personal project directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep personal project directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Keep live-session titles and personal directories out of the public diff.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
The import CTA lacked cursor-pointer, and the header export control used
the text xs size instead of the square icon-xs used by neighboring actions.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Reject unsafe session ids on native reads/writes, fail closed when lock
checks cannot run, keep Codex whitespace, preserve in-place provider
instances, and clear stale history/approvals when replacing a thread.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Macroscope expects new Effect services as a single canonical module with
the tag, make, and layer together. Inline dispatch-error construction at
the failure boundary instead of a curried helper.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Older remotes do not implement teleport RPCs. Advertise a teleport
capability from current servers and keep the web entry points hidden
when it is absent, including the React Native client until it opts in.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Teleport listed only the default provider home, so instances with a
custom homePath never appeared. Scan every configured instance, keep
the matching instance id on import, and skip unsafe path reuse.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
…tate

Reject history replace and export while a T3 session is live, skip OpenCode
message ids that would leave the storage root, and keep the header control
hoverable so its idle-state tooltip can show.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Failed lsof or open checks were reported as a locked native file, so a
missing probe looked like a live CLI lock. New Codex and Claude exports
also landed under the default home even when the thread used a custom
instance such as codex_work. Keep probe failures on their own error tag,
allocate files under the bound instance root, and import TeleportService
as a namespace at the WS boundary.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Threads on instances such as codex_work hid Teleport Out because the
header treated the instance id as a driver kind. Import could also load
the first matching session id across homes. Resolve export support from
the instance driver, pass providerInstanceId through the session ref,
and drop stale command-palette scans when the import view is left.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
The custom-instance export coverage was appended without removing the
original describe, so the capability checks ran twice.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
cursoragent and others added 8 commits August 27, 2026 06:38
Stat-then-readFile could still allocate a replacement or append that
grew past the cap. Stream the file with a hard byte limit and treat
overflow as oversize.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
…fails

normalizeDispatchCommand moves uploads onto the thread before the native
revision check. A rejected turn then skipped the dispatch tapError
cleanup and left those files orphaned.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
getThreadShellById and getThreadDetailById already load archived
threads, but latest-turn lookup still filtered archived_at IS NULL, so
import/UI paths lost turn state.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
An explicit providerInstances envelope replaces legacy providers.codex
config. Merging omitted homePath from the legacy field made teleport
scan or export to a different home than Codex itself.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Failed export fell back to a pending t3 sentinel when the directory row
was missing, wiping nativePath and nativeRevision so later turns stopped
watching the imported file.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
…overy

Clearing a leftover importing fence left the provider-directory binding
in place, so the next T3 turn could resume the native file without the
revision gate. Also treat native+importing directory lag as recoverable.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Canonicalize omitted providerInstanceId before the duplicate check, load
and commit each batch session sequentially, honor thread presence on
export, include archived worktree cwds, and delete leftover directory
rows when recovery clears an importing fence.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
…eSystem

Assigning wrapped.rename failed typecheck because FileSystem.rename is
readonly. Spread a new service object instead.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: a composer insertion guard was widened to every sendDisabledReason, which blocks ordinary draft input (file-tree mentions, terminal links, type-to-focus) while an image upload is in flight or the thread is still loading. Details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/ChatComposer.tsx Outdated
A later session that fails to load or unlock could leave earlier sessions
already imported while the batch RPC still failed. Validate every ref
first so those environmental failures leave zero imports.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the new apps/server/src/teleport/** service code against the Effect service conventions. Two findings on startup reconciliation error handling. The Effect.catchTag usages in formats/codex.ts and formats/claude.ts flagged in a previous run are still present and unchanged; not re-posting those.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/teleport/importTransaction.ts Outdated
Comment thread apps/server/src/teleport/TeleportService.ts Outdated
Comment thread apps/server/src/teleport/TeleportService.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI Consistency

One new finding on the teleport composer lock. The previously reported insertComposerTextAtEnd gate (apps/web/src/components/chat/ChatComposer.tsx L2803 — isSendDisabled also covers "Image still uploading" / "Sending feedback" / "Messages loading", so file-tree "Add to chat" and mention drops are rejected outside the teleport lock) is still present; not re-commented here.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/ChatComposer.tsx
cursoragent and others added 2 commits August 27, 2026 19:28
A transient directory.getBinding failure was treated as no binding, so
rollback could delete a real provider row. Abort the in-place import
instead. Startup recovery now retries once and re-fails interrupts so a
shutdown mid-repair cannot look successful.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
File-tree mentions and type-to-focus were blocked by every send-disabled
reason. Gate insertTextAtEnd on the teleport lock only, and disable
attachment and chip remove controls while that lock is held.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One convention finding in the new teleport format registry. Note that the two previously flagged Effect.catchTag usages in apps/server/src/teleport/formats/codex.ts and apps/server/src/teleport/formats/claude.ts are still present; those comments are not repeated here.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/teleport/formats/registry.ts Outdated
Comment thread apps/web/src/components/chat/ChatComposer.tsx
Comment thread apps/server/src/teleport/TeleportService.ts
cursoragent and others added 3 commits August 27, 2026 21:00
make only wrapped a synchronous fromAdapters call so the layer could
use Layer.effect. Nothing else consumed it.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
The compact button stayed enabled, injected /compact, then submit
no-oped on the teleport lock. Disable it and surface the lock reason.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
…very

Import recovery can restore presence to native while the directory row
is still importing. Finalize was reading the pre-recovery snapshot, so
it skipped the repair and left the stores inconsistent.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Comment thread apps/web/src/components/chat/ChatComposer.tsx
cursoragent and others added 3 commits August 27, 2026 21:21
An already-open slash/mention menu could still apply /plan, /default, or
replacements after the thread teleported out. Guard selection with the
same teleport draft lock as other mutations.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Match the write-path convention by recovering TeleportSchemaVersionError
via catchTags in Codex and Claude session listing.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Reject free-form ancestors such as / so a read-scoped client cannot
enumerate native CLI session titles and paths outside T3 projects.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Comment thread apps/server/src/teleport/TeleportService.ts
When a pending export never wrote a native file, flip the thread back to
T3 and delete the directory binding so a later export is not rejected as
already-native.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: apps/server/src/teleport/sessionFile.ts computes the native-session digest with a suppressed node:crypto import instead of acquiring the Effect Crypto service the sibling module already uses. Everything else (new TeleportService tag/make/layer shape, namespace imports, catchTags usage, startup-recovery retry/interrupt handling, and the tagged-error definitions in packages/contracts/src/teleport.ts) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/teleport/sessionFile.ts Outdated
Reuse nativeRevisionFromBytes in readNativeSessionFile so import and
observe paths share one SHA-256 implementation instead of node:crypto.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
@t3dotgg

t3dotgg commented Aug 28, 2026

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Two-way teleport with native CLI sessions combines several separable runtime and lifecycle systems. It needs a smaller product decision and staged implementation before code review will be useful.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotgg t3dotgg closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants