Co-authored-by: maria-rcks <maria@kuuro.net> Co-authored-by: Bilal Bakr <62337003+Bil0000@users.noreply.github.com> Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com> Co-authored-by: Vitalii Yehorov <vitalyiegorov@gmail.com> Co-authored-by: Jake Leventhal <jakeleventhal@me.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Alex Southwell <saphid@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Nicholas Wasmiller <derped@mineperial.com> Co-authored-by: PB <poilmb@gmail.com> Co-authored-by: Exotic <118054752+extoci@users.noreply.github.com> Co-authored-by: Yash Singh <saiansh2525@gmail.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Theo Browne <me@t3.gg> Co-authored-by: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Co-authored-by: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com> Co-authored-by: scratchyone <scratchywon@gmail.com> Co-authored-by: Dominic Roy <dominic@sdko.org> Co-authored-by: chukfinley <chuk@chuk.dev> Co-authored-by: Primož Ajdišek <bigpod@bigpod.si> Co-authored-by: benthecarman <benthecarman@live.com> Co-authored-by: NaveDanan <nave0712@gmail.com> Co-authored-by: aaditagrawal <103925638+aaditagrawal@users.noreply.github.com> Co-authored-by: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Co-authored-by: Nick Anisimov <n.anisimov.23@gmail.com> Co-authored-by: MacKinley Smith <smithmackinley@gmail.com> Co-authored-by: Yordis Prieto <yordis.prieto@gmail.com> Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: AKolenda <akole779@mtroyal.ca> Co-authored-by: Guillermo Casanova <75276669+Gigioxx@users.noreply.github.com> Co-authored-by: Otavio Salvador <otavio@ossystems.com.br> Co-authored-by: Shirish Pothi <183252392+shirishpothi@users.noreply.github.com> Co-authored-by: Ishaan Kothari <ishaanko.mail@gmail.com> Co-authored-by: Bob Fowler <bob@rjf.ca> Co-authored-by: Anton Bezdenezhnykh <gamer392@yandex.ru> Co-authored-by: ValeraZSD <48602572+ValeraZSD@users.noreply.github.com> Co-authored-by: Ephraim <ephraim39hr14m@gmail.com> Co-authored-by: Ryan Ilano <ryanilano@users.noreply.github.com> Co-authored-by: Alex <me@pixp.cc> Co-authored-by: maco <gosarmarcel7@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Tristan Knight <admin@snappeh.com> Co-authored-by: PR Batch Tester <agent@local.test> Co-authored-by: oliver <97427849+flamboh@users.noreply.github.com> Co-authored-by: kamkm <99585688+Kamkmgamer@users.noreply.github.com> Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
11 KiB
Thread Lineage And Context Transfer
Forking, provider handoff, merge-back, and subagents are separate product features, but they should share one orchestration model. The common primitive is not "fork" and not "summary". It is:
thread relationship
+ source point
+ optional context transfer resolution
The app should preserve lineage cheaply and resolve expensive provider/context work lazily when a run actually needs it.
Product Use Cases
Fork To Explore
A user forks an existing app thread to explore a tangent without polluting the source thread's context.
source AppThread
-> user forks from stable source point
-> new idle AppThread
-> no provider chosen yet
-> first message chooses provider/model
If the first run on the fork uses the same provider as the source and the provider supports native fork, use the provider-native fork path. If it uses a different provider, materialize portable context at first dispatch.
Continue Same Thread With A Different Agent
A user continues the same app thread with another provider. This is not a new app thread, but it is still a context transfer.
AppThread runs 1-5 with Codex
user sends run 6 with Claude
-> resolve context transfer from Codex-backed source state to Claude
-> start Claude provider thread with handoff context + user message
The app thread remains the canonical conversation. Provider-native threads are backing handles with explicit coverage.
Merge A Fork Back Into Its Source
A user explores deeply in a fork, then brings the result back to the source thread.
source thread at source point S
fork explores runs F1-Fn
user sends next message in source thread with "bring this back"
-> build delta from S to fork latest stable point
-> inject delta context with the source-thread user message
This is not a full source-thread provider switch. It is a targeted context transfer from the fork back to the original thread.
Subagents
Subagents are the same family of operation with a different creator and lifecycle contract.
Native subagents:
- the provider spawns the child through its native capability;
- the app observes native child refs/events;
- the child is modeled as a child execution node and, when useful, as a related app subthread.
Cross-provider T3 subagents:
- the parent agent calls an app-owned tool;
- the app creates a child app thread/run with a relationship to the parent;
- the child result is transferred back to the parent as a subagent result context transfer.
The shared model should make user forks and agent-created subthreads feel like the same graph shape with different createdBy and lifecycle policy.
Core Concepts
Thread Relationship
Thread relationship describes why a target thread or run exists relative to another thread.
type ThreadRelationshipKind =
"fork" | "provider_handoff" | "merge_back" | "subagent_spawn" | "subagent_result";
Relationships are product-level and provider-neutral. A fork is not the same thing as a provider-native fork. A provider-native fork is one possible resolution strategy for a fork relationship.
Source Point
Source point describes the exact state being transferred.
type ContextSourcePoint = {
threadId: ThreadId;
runId?: RunId;
checkpointId?: CheckpointId;
turnItemId?: TurnItemId;
providerThreadRef?: NativeThreadRef;
providerTurnRef?: NativeTurnRef;
};
Stable source points should prefer completed runs/checkpoints. Active runs are harder because provider chunks may still arrive and checkpoints may not be final.
Context Transfer
Context transfer is the durable operational record that connects source and target. It is cheap to create and may stay unresolved until first use.
type ContextTransfer = {
id: ContextTransferId;
type: ThreadRelationshipKind;
sourceThreadId: ThreadId;
targetThreadId: ThreadId;
sourcePoint: ContextSourcePoint;
basePoint?: ContextSourcePoint;
sourceProvider?: ProviderKind;
targetProvider?: ProviderKind;
status:
"pending" | "resolved_native" | "resolved_portable" | "failed" | "consumed" | "superseded";
resolution?: ContextTransferResolution;
createdBy: "user" | "agent" | "system";
createdAt: string;
consumedAt?: string;
};
basePoint is used for delta transfers, especially merge-back. For a fork merge-back, basePoint is the original fork source point and sourcePoint is the fork's latest stable point.
Context Handoff
Context handoff is the expensive materialized context artifact. It is created lazily only when native transfer is unavailable or insufficient.
type ContextHandoff = {
id: ContextHandoffId;
transferId: ContextTransferId;
kind: "portable_context" | "delta_context" | "checkpoint_context";
payload: PortableContextPayload;
createdAt: string;
};
Context handoffs should be auditable app artifacts, not hidden prompt concatenation.
Resolution Strategy
Resolution decides how the target provider/thread receives the source context.
type ContextTransferResolution =
| {
strategy: "native_fork";
providerThreadRef: NativeThreadRef;
}
| {
strategy: "portable_context";
contextHandoffId: ContextHandoffId;
}
| {
strategy: "delta_context";
contextHandoffId: ContextHandoffId;
}
| {
strategy: "checkpoint_context";
contextHandoffId: ContextHandoffId;
};
Same-provider forks should prefer native_fork when the source native refs are strong and the adapter supports it. Cross-provider handoff and merge-back usually require a context handoff.
Lazy Resolution
Fork creation should be cheap:
thread.fork
-> create target AppThread
-> record thread relationship / ContextTransfer(status=pending)
-> do not create provider session
-> do not create provider thread
-> do not build portable context
First dispatch on a pending fork resolves the transfer:
first run on fork chooses provider P
-> find pending fork ContextTransfer
-> if source provider == P and native fork is supported:
resolve native fork
else:
materialize portable context
-> create/resume provider thread
-> send handoff/native context + user message
This avoids forcing users to choose an agent at fork time and avoids generating summaries that may never be used.
Runtime Entry Point
Run startup should have one provider-neutral hook:
resolveStartContext({
threadId,
provider,
message,
}): StartContextResolution
This hook checks pending context transfers targeting the thread/run and chooses a strategy:
- No transfer needed: target provider already has current coverage.
- Native transfer: same provider and adapter supports native fork/resume.
- Portable transfer: build a context handoff and inject it into the run.
- Delta transfer: build the changes between
basePointandsourcePoint. - Unsupported: fail explicitly before provider work starts.
Provider adapters own native details. The orchestrator owns the relationship, source point, durable transfer record, and command receipts.
For Codex, native thread/fork accepts an inclusive lastTurnId boundary. When the app source point is a completed provider turn with a native turn reference, the Codex adapter passes that native id so the provider creates the fork at the requested point directly. This is the only viable path on paginated Codex threads, which reject thread/rollback. If no native turn reference is available, the adapter falls back to forking the latest native state and rolling back the fork by the number of later terminal provider turns — and reports an explicit failure when the forked thread uses paginated history, since that fallback cannot be honored there. The orchestrator still passes a provider-neutral source point and source provider-turn history; it does not encode Codex boundary or rollback policy.
The same paginated-history constraint applies to direct checkpoint rollback: thread/rollback only works on legacy-history Codex threads. The V2 adapter probes historyMode before rolling back and fails explicitly on paginated threads rather than sending a request Codex will reject. The paginated equivalent (page thread/turns/list to find the first removed turn, then thread/revert with beforeTurnId) is not implemented yet.
Data Ownership
AppThread should store lightweight browsing lineage:
type AppThreadLineage = {
parentThreadId: ThreadId | null;
relationshipToParent: "fork" | "subagent" | null;
rootThreadId: ThreadId;
};
Operational transfer details should live in ContextTransfer, not directly on AppThread, because a thread can participate in many transfers:
- created by fork;
- later switched to another provider;
- later merged back to its source;
- later spawned subagents.
Checkpoint And Active-Run Policy
The first implementation should prefer stable source points:
- completed run checkpoint;
- explicit checkpoint;
- idle provider thread with known coverage.
Forking or handoff from an active run should either:
- use the latest completed checkpoint, or
- be rejected until active-run semantics are designed.
Do not silently fork from partially streamed provider state. That weakens correlation and makes replay/recovery difficult.
Current Implementation Boundary
The first backend slice implements lazy same-provider fork resolution:
thread.forkcreates a target app thread with fork lineage and a pendingContextTransfer.thread.forkdoes not create a provider session, provider thread, provider turn, or portable context artifact.- The first dispatch on the fork resolves the pending transfer. For Codex-to-Codex forks with strong native refs and
thread/forksupport, the orchestrator uses the provider-native fork path and recordsresolved_nativethenconsumed. - Cross-provider or otherwise non-native fork transfers materialize a portable
ContextHandofflazily on the target thread's first dispatch. - Active source runs are rejected for explicit run/checkpoint forks;
latest_stableresolves to the latest completed checkpointed run.
This keeps the runtime aligned with the architecture while making portable context explicit and
auditable through ContextTransfer and ContextHandoff records.
Relationship To Provider Switching
Provider switching is a context transfer where sourceThreadId === targetThreadId and the target provider differs from the current active provider.
Returning to a prior provider is also a context transfer, usually a delta from the provider's last covered run range to the current app-thread point.
The Provider Switching And Context Handoff document describes strategy selection for that specific feature. This document defines the broader model shared by provider switching, forks, merge-back, and subagents.