Skip to content

feat(handoff): continue threads across environments with any provider - #52

Merged
BarretoDiego merged 1 commit into
mainfrom
feature/handoff-all-providers
Sep 11, 2026
Merged

BarretoDiego merged 1 commit into
mainfrom
feature/handoff-all-providers

Conversation

@BarretoDiego

Copy link
Copy Markdown
Owner

Codex threads had no visible handoff action because the existing transfer only supported Claude native sessions. Users can now move a thread to another environment with any registered provider, including Codex → OpenCode, and explicitly select the destination model.

The client distinguishes Native session (compatible Claude sessions) from Conversation context (a new provider-native session with the complete T3 conversation). This change prioritizes transfer between environments; switching providers within one environment remains separate work.

Behavior and architecture

  • Preserve the existing same-thread ownership transaction, generation fencing, safe point, Git snapshot, authenticated Project Sync transport, verification and rollback. No new transport, secret-copy policy, artificial commits or simultaneous execution owners.
  • Add explicit context capabilities and transfer mode to shared contracts. Claude, Codex, Cursor, Grok, OpenCode and Antigravity use the common provider input path; orchestration does not branch on their SDKs.
  • Persist a validated, checksummed conversation archive and a pending context reference. Enrich the next real user turn with the complete archive, inline when it fits or through an explicit full-file read instruction. Never send a fabricated continuation message or reuse the source provider cursor for a different provider.
  • Consume pending context only after provider admission; serialize concurrent initial admissions and preserve pending context on failure or restart. If admission succeeds but persistence fails, a retry may repeat context; this is not distributed exactly-once delivery.
  • Validate destination provider/model before freezing source. Rollback removes prepared context even if the server fell between archive publication and metadata persistence. Native verification/install timing instrumentation remains intact.
  • Reuse existing Git-aware synchronization: available objects skip transfer, missing objects travel in bundles, and index/working-tree deltas plus relevant untracked files restore into a separate checkout. Existing ignored/generated-file and secret rules apply. Clients broker the existing authenticated node transport; no new SSH/Tailscale protocol selection is introduced.
  • Web/desktop and mobile expose provider/model selection, current readiness, progress and recovery. Fix a provider-refresh inspection loop and stale readiness. Align modal body/header/footer padding, retain a scrollable body and visible actions on narrow screens, and show readable selection labels.

Validation

  • 224 handoff/provider/coordinator tests passed across 11 files; coverage includes all six destination adapters, Codex → OpenCode, preserved user text, complete history, cursor isolation, rollback/restart, concurrent admission and destination model rejection.
  • 119 Project Sync tests passed. One existing HTTP malformed-frame test fails with UND_ERR_SOCKET instead of its expected 500 response; reproduced independently in the pre-change feature/thread-handoff worktree at dbeafa2b26 as well as this branch. No Project Sync HTTP/framing implementation changed in this PR.
  • Typecheck passed for contracts, client-runtime, server, web, mobile and desktop. Targeted lint/format and final diff checks passed.
  • Web, server and Electron builds passed. iOS and Android production JS exports passed.
  • Authorized real-browser pass against isolated worktree state, using imported conversation history: Codex thread menu opens Continue on…, context mode initializes successfully, desktop/390px layouts align without horizontal overflow, body scrolling exposes the safe-point selector while actions remain visible. No live installation data was changed.

Limitations

Native session migration remains Claude-only. Context transfer does not preserve another provider's internal compaction/tool state; large archives require the destination agent to read the provided file. Destination credentials are independent. Existing restrictions remain: attachments, return to an environment containing an earlier copy, and automatic project cloning are blocked. Runtime processes/local services are not migrated.

Provider execution was tested with fakes; no two-physical-machine transfer with real providers or native mobile simulator run was performed. Phase timing instrumentation exists, but no production-network latency claim is made. Clients and both environment servers must be updated; creating/merging this PR does not replace an already running Nightly binary.

Implemented with GPT-6 through the Codex harness.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 8, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 591f6b0.

This comment will update automatically after the next completed run.

@BarretoDiego BarretoDiego left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

AI-assisted review

The handoff archive path accepts an unrestricted handoff ID, allowing path traversal outside the intended state directory.

The changed tests do not verify that the conversation archive is delivered to or consumed by the destination provider. No production defect is proven from the supplied diff alone.

The context handoff loses the explicitly selected destination model when the destination session is recreated after restart.

No actionable issues found in the supplied changed files.

No actionable correctness, security, performance, or testing findings were identified in the supplied changed-file diff.

async function ownedDirectory(stateDir: string, handoffId: ThreadHandoffId, create: boolean) {
const base = await NodeFSP.realpath(stateDir);
let directory = base;
for (const segment of ["handoff-context", handoffId]) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

major: Validate handoff IDs before using them as filesystem path segments

ownedDirectory joins the caller-provided handoffId directly into a filesystem path. Because the reference decoder does not restrict the ID to a safe single path segment, a value such as ../../outside can make context installation, loading, or removal operate outside stateDir. The existing test at ConversationHandoffContext.test.ts:135 demonstrates that traversal-form IDs are accepted, although its ../outside value only escapes the handoff-context subdirectory.

const configuration = yield* Effect.try({
try: () => latestConfiguration(mapped),
catch: normalize,
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

major: Context restart ignores the selected destination model

When a context-mode session is recreated after restart, this call builds modelSelection from configuration.modelSelection rather than pending.destination.modelSelection. The destination may therefore start with the source model, or fail when that model is unavailable on the destination provider, despite the destination model having been validated during preparation.

Effect.gen(function* () {
const f = yield* makeTwoServices(provider);
yield* f.prepare;
const verified = yield* f.destinationService.handle({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

minor: Context tests do not verify conversation archive delivery

The six context-handoff cases assert that the destination has no native session ID and that a new cursor is used, but never assert that the complete conversation archive is passed to or consumed by the destination provider.

@BarretoDiego
BarretoDiego merged commit f12065d into main Sep 11, 2026
7 of 17 checks passed
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.

1 participant