Skip to content

Draft: two-way teleport with native CLI sessions - #15

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

Draft: two-way teleport with native CLI sessions#15
baanish wants to merge 133 commits into
mainfrom
cursor/teleport-7136-mirror-4ce4

Conversation

@baanish

@baanish baanish commented Aug 21, 2026

Copy link
Copy Markdown
Owner

What Changed

Local mirror of closed upstream pingdotgg/t3code#7136: two-way import/export of native Codex and Claude CLI sessions, plus native-revision watch and fork recovery.

TeleportProvider remains codex | claudeAgent only. Grok/OpenCode native teleport formats are not in this PR.

This revision addresses Codex and Kilo review on the mirror:

  • Fall back to an exclusive-open lock probe when lsof cannot be spawned.
  • Load archived threads by id so in-place import can unarchive the canonical native thread. Turn start is rejected on archived threads in the decider (not by hiding those rows from by-id reads).
  • Walk each Claude projectsRoot once per home instead of once per worktree.
  • Fail export with unknown-instance instead of writing into the default CLI home.
  • Ignore a previous thread's native-conflict result after navigation.
  • Restore the provider-directory binding if in-place import reverts.
  • Prefer the newer shell/detail teleport snapshot by lastSyncedAt.
  • Pass nativePath through command-palette import refs.
  • Do not persist a teleport-pending: path into the provider directory when interrupted export never wrote a file.

Adversarial-review ship-blockers (this pass):

  1. Failed/interrupted first-time in-place import no longer looks successful. Internal-only thread.teleport.clear emits thread.teleported with teleport: null. Crash recovery clears leftover importing when there is no prior native identity, instead of flipping to t3 with the uncommitted nativeRevision. Prior native teleport still restores (nativeimportingnative).
  2. WS thread.turn.start with bootstrap.createThread skips requireNativeRevisionForTurn so a missing id can create+dispatch. Existing-thread turn.start, including prepareWorktree-only bootstrap, still checks. Service fail-closed for a missing thread is unchanged.
  3. HTTP /api/orchestration/dispatch runs the same native-revision gate as WS, then cleans up failed uploaded attachments on dispatch error. Bugbot (valid): the HTTP path never runs thread.create, and the decider ignores bootstrap, so a bootstrap.createThread payload on an already-imported thread used to skip the gate. HTTP now looks up the thread and still requires the native revision when that id exists. The skip remains only for a missing thread.
  4. Chat composer keeps the native/importing teleport lock while messages load. Loading is an additional send disable, not a replacement; isTeleportSendDisabledReason still wins so prompt/stash cannot mutate. Attachment-upload blocking is also additional, not a replacement for the teleport lock.

Related high-confidence holes:

  1. Explicit nativePath in-place matching is path-stable: two files with the same session id do not rewrite each other's threads. Instance-id matching is unchanged.
  2. Failed-import directory revert restores identity fields, not a pre-stop status: "running" / starting binding or a teleport-pending: native path.

Must-fix pass (mergeability):

  1. Equal lastSyncedAt now prefers the shell teleport snapshot (>=, not >). A same-timestamp ownership flip (t3 → importing, native → t3) no longer keeps stale cached detail.
  2. Migration 044 backfills teleport_json only when provider_name is already a valid TeleportProvider (codex | claudeAgent). Leftover opencode / grok runtime rows are skipped; no invented mapping.
  3. Restored OpenCodeAdapter.ts / .test.ts to origin/main. The abort-gate / unconfirmed-abort residue is not Codex/Claude teleport.

Atomicity pass (6/10 → ~9/10):

  1. beginImporting is inside the same uninterruptible revert region as stop / directory / commit. Interrupt or defect during or immediately after the fence still reverts presence: "importing".
  2. First-time in-place import with no prior directory row deletes that row on revert. A prior binding still restores identity fields only (no running / teleport-pending:).
  3. Startup recovery deletes a leftover new-thread import (restorePresence: "t3", empty or fence-only history, still importing). In-place imports of existing threads are cleared or restored, not deleted.
  4. Startup recovery finalizes a provider-directory payload that lagged behind a committed T3 import (orchestration.presence === "t3" while directory is importing, missing, or pending). Failures are logged and skipped.
  5. Unix lock probe is fail-closed when lsof cannot be spawned and the target file exists (TeleportLockProbeError). Missing file (ENOENT) stays unlocked so first export create can proceed. Windows exclusive-open is unchanged.
  6. Windows native replace restores from .teleport-bak if the replacement rename fails; a failed restore now fails the operation instead of swallowing a lost original. Unix replace remains POSIX rename. Tests cover both paths and verify-does-not-clobber.
  7. withInFlight is documented as process-local. It is not a cluster or cross-process lock.

Merged latest origin/main (a3a8cbd60) into this branch (e64e31ac8426cb0edf743a144fc8879f47e6a84b). Bugbot HTTP gate follow-up is 93d32f2d3. Current HEAD is ca3e9b78c. CI must be green on this exact SHA.

Conflict resolutions in this merge (all simple / keep-both; no complicated intent conflicts):

  • Auth session client-connection stays 041. Main took 042 (linked PR) and 043 (unsettledAt); teleport projection is 044.
  • Projection thread rows, snapshot queries, and environment capabilities keep both linkedPullRequest / threadPullRequestLinking and teleport.
  • ChatComposer: HEIC prepareImageForAttachment from main, plus HEAD's in-flight abort when send becomes disabled (teleport lock still first).

Earlier main merge notes still apply:

  • WebSocket turn dispatch still verifies native revision, then stamps client origin.
  • HTTP /api/orchestration/dispatch uses the same gate, then cleanupFailedUploadedAttachments on tapError.
  • ChatView send-disabled order: teleport → "Sending feedback""Messages loading" → native-conflict. Teleport lock is never replaced by loading.
  • ProviderCommandReactor interrupt recovery remains main fix(server): recover from provider interrupt failures pingdotgg/t3code#7412 (interruptTurn fail → stopSession → mark stopped with lastError). OpenCode zombie Stop is not restored.
  • Orchestration events keep teleport payloads and optional client origin.
  • Engine tests cover origin stamping and teleport import atomicity.

Still not 10/10 (cannot be without new stores):

  • No cross-store transaction (orchestration + provider directory + native file).
  • withInFlight is a process-local mutex, not a cluster lock.
  • A CLI can still write the native file during an admitted T3 turn after the revision check.
  • TELEPORT_IMPORT_BATCH_SEMANTICS stays per-session.

Not changing in this pass:

  • Mobile teleport-in UI. Mobile already blocks send while teleported out; reverse import stays on web/desktop.
  • Symlink cycle walks / uncapped post-stat read.
  • Export verify using T3 parser vs CLI serde.
  • Parser O(n²) Claude rewind / 20MB newline fuzz corpus.

Why

Successful in-place import must be one orchestration transition (thread.teleport.import). Failed or interrupted import must revert presence: "importing" and stay retryable — including first-time directory-bound threads that never had a teleport document. After import, SHA-256 native revision watch blocks turns when the CLI file diverged or went missing; Fork native changes copies that transcript into a new T3 thread. The revision gate must apply on every thread.turn.start ingress (WS and HTTP) except bootstrap.createThread on a thread that does not exist yet.

Equal-timestamp shell/detail merges must not hide a live ownership flip. Projection backfill must not write schema-invalid teleport_json.provider values. This PR should not carry stacked OpenCode abort-gate adapter diffs.

Live interrupt after the import fence, first-time directory upserts, new-thread crash husks, and a directory that never finalized after a successful T3 commit must not strand importing until process restart. Unix hosts without lsof must not fail-open on an existing native file.

Stacked sibling remains PR #9 (cursor/teleport-opencode-1e23). This mirror targets main.

UI Changes

Media copied from closed upstream pingdotgg/t3code#7136. Import lives on the new-thread / sessions view. Export lives in the chat header. Composer send is disabled while the thread is in the native CLI.

New UI

Draft hero Import a native CLI session link

Command palette Import sessions action

Native Claude and Codex sessions in the import picker

Hover

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

After import, SHA-256 native revision watch shows this banner when the CLI file diverges. 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

7136 linked these as clickable posters. The raw .mp4 files have no public download URL from this environment.

Codex

Codex teleport roundtrip (T3 export → native dark mode → import)

Claude

Claude teleport roundtrip (T3 export → native dark mode → import)

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
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added Teleport support for discovering, importing, and exporting native CLI sessions.
    • Added support for Codex and Claude session formats.
    • Added native revision checks and the ability to fork diverged sessions.
    • Added Teleport controls, import flows, status indicators, and conflict banners.
    • Added environment capability detection and command palette access to session imports.
  • Bug Fixes

    • Prevented sending and editing while threads are importing, native-controlled, or conflicted.
    • Improved interruption recovery and session state restoration.
    • Preserved thread metadata across history replacement and archived-thread loading.

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 17 commits August 27, 2026 03:20
Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
…ismatch

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
The Windows rename mock treated every rename onto the destination as the
failed replacement, including bak → dest. That made the successful-restore
path look like restore-must-fail and hid the original session file.

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

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
A refreshed shell with teleport: null is newer than a cached detail
snapshot. Keeping the stale detail left the composer locked after
presence was cleared.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
Pending or failed image uploads still disable send, but they must not
refuse a second paste. Draft mutation already used the teleport-specific
predicate everywhere else in this flow.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
isSyntheticNativeUserText is for injected CLI user prompts. Applying it
to assistant responses dropped legitimate imported replies whose text
started with the same markers.

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

Two native files can share an externalSessionId. Skipping the real-path
check unless the client sent nativePath let import replace the bound
thread with a different file.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
stat follows symlink targets, so a sessions/loop -> sessions link
re-entered the same tree forever. Track visited canonical directories
before descending.

Co-authored-by: aanishbhirud <aanishbhirud@gmail.com>
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>
const importThreadShells = [...activeShell.threads, ...archivedShell.threads];

for (const ref of input.sessions) {
const loaded = yield* loadTeleportSession({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING]: Import batch is no longer all-or-nothing for load/unlock failures

Previously an upfront loop loaded + requireParsedSessionUnlocked-checked every session before any thread was committed, so a session that failed to load (missing file, unsupported provider, parse error) or was locked by a running native CLI aborted the whole batch before anything changed. That upfront validation loop was removed in 5798b445, and load + unlock now run inside the per-ref commit loop, right before each session is committed via engine.dispatch + directory.upsert.

If session N fails to load/unlock after sessions 1..N-1 were already committed, the request returns the load error while the earlier sessions were genuinely imported. The client sees a failed batch but partial state now exists, and a blind retry then hits the in-place / identity-conflict path for the already-imported sessions.

The commit message documents the deliberate "load and commit each batch session sequentially" refactor (for identity handling), but the load/unlock phase does not depend on per-ref commit context, so the upfront all-or-nothing validation could be restored while keeping the sequential commit. Worth a maintainer decision on whether partial commit is acceptable for these environmental failures.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

cursoragent and others added 8 commits August 27, 2026 07:07
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>
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>
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>
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agentic security review of the latest head found one medium issue: teleport.listSessions accepts a free-form cwd and treats / (or $HOME) as a containing root, so a read-scoped paired client can enumerate native CLI session titles and paths outside any T3 project. Import already binds cwd to a project workspace; list should do the same.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread apps/server/src/teleport/TeleportService.ts
cursoragent and others added 3 commits August 28, 2026 04:52
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>
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 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.

2 participants