Draft: two-way teleport with native CLI sessions - #15
Conversation
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>
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({ |
There was a problem hiding this comment.
[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.
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>
There was a problem hiding this comment.
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.
Sent by Cursor Security Agent: Security Reviewer
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>


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.
TeleportProviderremainscodex | claudeAgentonly. Grok/OpenCode native teleport formats are not in this PR.This revision addresses Codex and Kilo review on the mirror:
lsofcannot be spawned.projectsRootonce per home instead of once per worktree.unknown-instanceinstead of writing into the default CLI home.lastSyncedAt.nativePaththrough command-palette import refs.teleport-pending:path into the provider directory when interrupted export never wrote a file.Adversarial-review ship-blockers (this pass):
thread.teleport.clearemitsthread.teleportedwithteleport: null. Crash recovery clears leftoverimportingwhen there is no prior native identity, instead of flipping tot3with the uncommittednativeRevision. Prior native teleport still restores (native→importing→native).thread.turn.startwithbootstrap.createThreadskipsrequireNativeRevisionForTurnso 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./api/orchestration/dispatchruns the same native-revision gate as WS, then cleans up failed uploaded attachments on dispatch error. Bugbot (valid): the HTTP path never runsthread.create, and the decider ignoresbootstrap, so abootstrap.createThreadpayload 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.isTeleportSendDisabledReasonstill wins so prompt/stash cannot mutate. Attachment-upload blocking is also additional, not a replacement for the teleport lock.Related high-confidence holes:
nativePathin-place matching is path-stable: two files with the same session id do not rewrite each other's threads. Instance-id matching is unchanged.status: "running"/startingbinding or ateleport-pending:native path.Must-fix pass (mergeability):
lastSyncedAtnow prefers the shell teleport snapshot (>=, not>). A same-timestamp ownership flip (t3 → importing,native → t3) no longer keeps stale cached detail.044backfillsteleport_jsononly whenprovider_nameis already a validTeleportProvider(codex|claudeAgent). Leftoveropencode/grokruntime rows are skipped; no invented mapping.OpenCodeAdapter.ts/.test.tstoorigin/main. The abort-gate / unconfirmed-abort residue is not Codex/Claude teleport.Atomicity pass (6/10 → ~9/10):
beginImportingis inside the same uninterruptible revert region as stop / directory / commit. Interrupt or defect during or immediately after the fence still revertspresence: "importing".running/teleport-pending:).restorePresence: "t3", empty or fence-only history, stillimporting). In-place imports of existing threads are cleared or restored, not deleted.orchestration.presence === "t3"while directory isimporting, missing, or pending). Failures are logged and skipped.lsofcannot be spawned and the target file exists (TeleportLockProbeError). Missing file (ENOENT) stays unlocked so first export create can proceed. Windows exclusive-open is unchanged..teleport-bakif 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.withInFlightis 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 is93d32f2d3. Current HEAD isca3e9b78c. CI must be green on this exact SHA.Conflict resolutions in this merge (all simple / keep-both; no complicated intent conflicts):
041. Main took042(linked PR) and043(unsettledAt); teleport projection is044.linkedPullRequest/threadPullRequestLinkingandteleport.ChatComposer: HEICprepareImageForAttachmentfrom main, plus HEAD's in-flight abort when send becomes disabled (teleport lock still first).Earlier
mainmerge notes still apply:/api/orchestration/dispatchuses the same gate, thencleanupFailedUploadedAttachmentson tapError."Sending feedback"→"Messages loading"→ native-conflict. Teleport lock is never replaced by loading.ProviderCommandReactorinterrupt recovery remains main fix(server): recover from provider interrupt failures pingdotgg/t3code#7412 (interruptTurnfail →stopSession→ markstoppedwith lastError). OpenCode zombie Stop is not restored.Still not 10/10 (cannot be without new stores):
withInFlightis a process-local mutex, not a cluster lock.TELEPORT_IMPORT_BATCH_SEMANTICSstays per-session.Not changing in this pass:
Why
Successful in-place import must be one orchestration transition (
thread.teleport.import). Failed or interrupted import must revertpresence: "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 everythread.turn.startingress (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.providervalues. 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
importinguntil process restart. Unix hosts withoutlsofmust not fail-open on an existing native file.Stacked sibling remains PR #9 (
cursor/teleport-opencode-1e23). This mirror targetsmain.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
Hover
Composer disabled after Teleport Out
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.
Recordings
7136 linked these as clickable posters. The raw
.mp4files have no public download URL from this environment.Codex
Claude
Checklist
Summary by CodeRabbit
New Features
Bug Fixes