Skip to main content
A session represents an active connection between T3 Code and a provider (currently Codex). Sessions manage conversation threads, turn execution, and maintain stateful connections to the underlying agent infrastructure.

Session Lifecycle

Sessions progress through distinct states during their lifetime:
1

Connecting

The server spawns a codex app-server process and establishes JSON-RPC communication over stdio.
2

Ready

The session is idle and ready to accept new turns. No active agent execution is in progress.
3

Running

A turn is actively executing. The agent is processing user input, running tools, or generating responses.
4

Error

A recoverable error occurred. The session may be able to continue with intervention.
5

Closed

The session has been terminated and can no longer accept commands.

Session Schema

Sessions are represented by the ProviderSession type:
packages/contracts/src/provider.ts
export const ProviderSession = Schema.Struct({
  provider: ProviderKind,              // "codex"
  status: ProviderSessionStatus,        // Current state
  runtimeMode: RuntimeMode,             // "full-access" | "approval-required"
  cwd: Schema.optional(TrimmedNonEmptyStringSchema),
  model: Schema.optional(TrimmedNonEmptyStringSchema),
  threadId: ThreadId,
  resumeCursor: Schema.optional(Schema.Unknown),
  activeTurnId: Schema.optional(TurnId),
  createdAt: IsoDateTime,
  updatedAt: IsoDateTime,
  lastError: Schema.optional(TrimmedNonEmptyStringSchema),
});
The resumeCursor field stores provider-specific state needed to resume a thread after restart. For Codex, this contains the provider’s internal thread ID.

Starting a Session

Sessions are started via the ProviderService.startSession method:
interface ProviderSessionStartInput {
  threadId: ThreadId;
  provider?: "codex";
  cwd?: string;                    // Working directory
  model?: string;                  // Model identifier
  resumeCursor?: unknown;          // Resume existing thread
  runtimeMode: RuntimeMode;        // Approval policy
  providerOptions?: {              // Provider-specific config
    codex?: {
      binaryPath?: string;
      homePath?: string;
    };
  };
}
const session = await providerService.startSession(threadId, {
  threadId,
  provider: "codex",
  cwd: "/path/to/workspace",
  model: "gpt-5.4",
  runtimeMode: "full-access",
});

Session Resume Behavior

When a resumeCursor is provided, T3 Code attempts to resume the existing Codex thread:
1

Resume Attempt

Send thread/resume JSON-RPC request to Codex app-server with the provider thread ID
2

Success Path

If resume succeeds, the session reconnects to the existing thread state
3

Fallback Path

If resume fails with a recoverable error (thread not found, missing thread), automatically fall back to starting a fresh thread
4

Error Path

If resume fails with an unrecoverable error, the session transitions to error state
apps/server/src/codexAppServerManager.ts
if (resumeThreadId) {
  try {
    threadOpenMethod = "thread/resume";
    threadOpenResponse = await this.sendRequest(context, "thread/resume", {
      ...sessionOverrides,
      threadId: resumeThreadId,
    });
  } catch (error) {
    if (!isRecoverableThreadResumeError(error)) {
      throw error; // Unrecoverable
    }
    
    // Fallback to fresh start
    threadOpenMethod = "thread/start";
    threadOpenResponse = await this.sendRequest(
      context,
      "thread/start",
      threadStartParams
    );
  }
}
Recoverable Resume Errors: Errors containing snippets like “not found”, “missing thread”, “no such thread”, “unknown thread”, or “does not exist” trigger automatic fallback to a fresh thread start.

Session Events

Sessions emit lifecycle events through the provider event stream:
Emitted when the session begins connecting to the provider.
{
  kind: "session",
  method: "session/connecting",
  message: "Starting codex app-server"
}
Emitted when attempting to open or resume a thread.
{
  kind: "session",
  method: "session/threadOpenRequested",
  message: "Attempting to resume thread abc123."
}
Emitted when resume fails and falls back to fresh start.
{
  kind: "session",
  method: "session/threadResumeFallback",
  message: "Could not resume thread abc123; started a new thread instead."
}
Emitted when the session is ready to accept turns.
{
  kind: "session",
  method: "session/ready",
  message: "Connected to thread abc123"
}
Emitted when the session has been closed.
{
  kind: "session",
  method: "session/closed",
  message: "Session stopped"
}

Sending Turns

Once a session is ready, you can send turns to the agent:
interface ProviderSendTurnInput {
  threadId: ThreadId;
  input?: string;                        // User message text
  attachments?: ChatAttachment[];        // Images, etc.
  model?: string;                        // Override model
  interactionMode?: "default" | "plan";  // Interaction mode
}

const result = await providerService.sendTurn({
  threadId,
  input: "Add error handling to the API routes",
  interactionMode: "default",
});

// result.turnId - Identifier for this turn
// result.resumeCursor - Updated resume state
When a turn starts, the session transitions from ready to running state and sets activeTurnId to the new turn’s ID.

Turn Lifecycle

Turns represent discrete agent execution cycles:
type TurnState =
  | "running"      // Turn is executing
  | "interrupted"  // User interrupted
  | "completed"    // Turn finished successfully
  | "error";       // Turn failed

Turn Events

Turns emit various events during execution:

turn/started

Turn execution has begun

turn/completed

Turn finished (success or error)

item/agentMessage/delta

Streaming text from agent response

item/tool/started

Tool execution started

Interrupting Turns

Active turns can be interrupted:
await providerService.interruptTurn({
  threadId,
  turnId, // Optional: defaults to active turn
});
When interrupted:
  1. Session status changes to “ready”
  2. activeTurnId is cleared
  3. Partial work may be retained depending on provider behavior

Stopping Sessions

Sessions can be explicitly stopped:
await providerService.stopSession({ threadId });
Stopping a session:
  1. Cancels any active turns
  2. Rejects pending approval requests
  3. Kills the codex app-server process
  4. Removes the session from the active session directory
  5. Emits session/closed event
Stopped sessions cannot be restarted. You must create a new session, optionally with a resumeCursor to continue the conversation.

Session Directory

The ProviderSessionDirectory tracks active sessions:
interface ProviderSessionDirectory {
  // Register a new session
  register(session: ProviderSession): Effect;
  
  // Update session state
  update(threadId: ThreadId, updates: Partial<ProviderSession>): Effect;
  
  // Get session by thread ID
  get(threadId: ThreadId): Effect<ProviderSession | undefined>;
  
  // Remove session
  remove(threadId: ThreadId): Effect;
  
  // List all active sessions
  list(): Effect<ReadonlyArray<ProviderSession>>;
}

Provider-Specific Session Features

Codex Sessions

Codex sessions support additional capabilities:
Read the full conversation history:
const snapshot = await codexAdapter.readThread(threadId);
// snapshot.turns - Array of turns with items
Rewind conversation state by N turns:
await codexAdapter.rollbackThread(threadId, numTurns);
Codex sessions detect account type and plan:
type CodexAccountType = "apiKey" | "chatgpt" | "unknown";

// Affects model availability (e.g., Spark model restrictions)

Best Practices

Always Store Resume Cursor

Save resumeCursor after each successful operation to enable seamless restarts

Handle Resume Failures

Be prepared for resume to fall back to fresh thread start

Monitor Session Status

Watch for status transitions to handle errors and completion

Clean Up Sessions

Explicitly stop sessions when done to free resources

Next Steps

Runtime Modes

Configure approval policies and sandbox modes

Providers

Learn about provider adapter architecture

Architecture

Understand the overall system design

Build docs developers (and LLMs) love