Two-way teleport with native CLI sessions - #8357
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>
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>
There was a problem hiding this comment.
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
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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>
There was a problem hiding this comment.
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
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>
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>
There was a problem hiding this comment.
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
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>
|
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. |
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 —
t3ornative— 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:
TeleportProvideriscodex | claudeAgentonly. 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:
lsof; iflsofcan'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.nativeorimporting. The message-loading and attachment-upload disables stack on top of that lock — they don't replace it.thread.turn.start, over WebSocket or HTTP (/api/orchestration/dispatch), requires a matching native revision. The one exception isbootstrap.createThreadwhen the thread doesn't exist yet.Recovery
In-place import is a single orchestration transition (
thread.teleport.import). A failed or interrupted import revertspresence: importingand 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_jsoncolumn (migration044) and backfills only validTeleportProvidervalues.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
Tooltips
Composer disabled after teleport out
Native-revision conflict banner
Shown when the revision watch detects the CLI file diverged after import. Send is blocked until the user forks.
Recordings
Codex — full roundtrip: T3 export → native CLI (dark mode) → import back
Claude — full roundtrip: T3 export → native CLI (dark mode) → import back
Checklist
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 handlethread.history-replacedby 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.requireNativeRevisionForTurnbeforethread.turn.start(skipped for bootstrap create on a missing thread); teleport RPCs get auth scopes; environment advertisescapabilities.teleport. Provider changes treatturn.abortedlike completion for session state and stop recovering stale sessions oninterruptTurn.Mobile mirrors web fail-closed behavior: the thread composer disables send and shows
teleportSendDisabledReasonwhen 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)
TeleportServicethat 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.thread.history.replace,thread.teleport.set,thread.teleport.import, andthread.teleport.clear, plus corresponding eventsthread.history-replacedandthread.teleported; the decider blocks turn starts on native/importing presence and can unarchive threads on import.teleport_jsoncolumn onprojection_threads(migration 44) and hydrates it through projection handlers, snapshot queries, and client-side thread reducers.TeleportOutButtonin 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.interruptTurninmakeProviderServicenow passesallowRecovery: false, preventing stale-session recovery before an interrupt.thread.turn.startmay now fail early with a native-revision check viaturnStartRequiresNativeRevisionCheckandTeleportService.requireNativeRevisionForTurnbefore dispatch in both HTTP and WS paths; archived threads are returned by by-id snapshot queries (list queries still exclude them).Macroscope summarized a6b7b47.