Skip to content

feat(codex): import existing sessions - #5146

Open
shashwatgokhe wants to merge 1 commit into
pingdotgg:mainfrom
shashwatgokhe:agent/codex-session-import
Open

feat(codex): import existing sessions#5146
shashwatgokhe wants to merge 1 commit into
pingdotgg:mainfrom
shashwatgokhe:agent/codex-session-import

Conversation

@shashwatgokhe

@shashwatgokhe shashwatgokhe commented Jul 31, 2026

Copy link
Copy Markdown

What changed

Adds an opt-in Import Codex sessions flow from a project's settings.

  • Discovers eligible native Codex conversations through Codex app-server APIs, scoped to the project path.
  • Imports selected sessions as T3 conversation snapshots, then stores a strict continuation binding to the original Codex thread.
  • Keeps the source conversation in Codex unchanged; existing T3/Codex session behavior is unchanged.
  • Adds clear UI states for no provider, unavailable history, empty results, imported sessions, stale selections, and the 50-session batch limit.
  • Documents the behavior and its deliberate boundary: this is not an automatic two-way mirror.

Why

This addresses #207 with a safe migration path for people moving work between Codex and T3 Code. It preserves access to the original Codex conversation while making the imported context visible as a T3 thread.

Implementation notes

  • Uses Codex's supported app-server thread/list, thread/read, and thread/resume capabilities rather than parsing or moving Codex session files.
  • Imports text history atomically, marks the new thread stopped, and avoids falling back to a fresh provider session if its original Codex thread no longer exists.
  • Detects already-imported source thread IDs so repeated imports are idempotent.

UI verification

Verified in an isolated local T3 environment:

  • Project settings exposes a dedicated Codex sessions card.
  • The import dialog renders the project scope, explicit snapshot/continuation explanation, refresh action, empty state, and disabled zero-selection import action.

Validation

  • corepack pnpm exec vp test run apps/server/src/provider/Layers/CodexSessionImport.test.ts apps/server/src/provider/Layers/CodexSessionRuntime.test.ts apps/server/src/orchestration/decider.historyImport.test.ts apps/server/src/environment/ServerEnvironment.test.ts apps/server/src/server.test.ts
    • 144 tests passed across 5 files
  • corepack pnpm --filter t3 typecheck
  • corepack pnpm --filter @t3tools/contracts typecheck
  • corepack pnpm --filter @t3tools/client-runtime typecheck
  • corepack pnpm --filter @t3tools/web typecheck
  • corepack pnpm exec oxfmt --check on the changed files
  • git diff --check

Checklist

  • Rebased on current main
  • Added focused server, runtime, orchestration, environment, and WebSocket coverage
  • Updated the Codex provider documentation
  • No source Codex session files are moved, copied, or mutated

Implemented with Codex desktop (GPT-5.6 Terra) through T3 Code.


Note

Medium Risk
Touches orchestration event projection, provider session bindings, and Codex resume semantics; mistakes could create duplicate threads or break continuation on imported conversations, though idempotent command ids and strict resume cursors limit blast radius.

Overview
Adds an Import Codex sessions flow so users can adopt existing native Codex conversations into a T3 project from project settings.

The server exposes codex.listSessions and codex.importSessions WebSocket RPCs (read vs operate scopes), advertises codexSessionImport on the environment descriptor, and wires a CodexSessionImport service that lists sessions scoped to the project workspace, reads history through Codex app-server (thread/list, thread/read), and imports via a new internal thread.history.import orchestration command that creates a stopped thread with a text-only message snapshot in one transaction.

Imported threads get deterministic ids and strict resume cursors (requireExistingThread: true) so openCodexThread does not fall back to a fresh native thread if the original was deleted; ordinary Codex continuations keep the existing resume fallback. CodexThreadHistory on the Codex driver filters ephemeral/sub-agent/exec sessions and caps discovery at 500 results.

The web app adds CodexSessionImportDialog (provider picker, refresh, batch limit of 50, already-imported state) and a sidebar project-settings entry gated on the capability flag. Client-runtime adds environment-scoped list/import atoms; contracts and user docs describe the deliberate non-mirror boundary.

Reviewed by Cursor Bugbot for commit 0ac6907. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Codex session import feature to discover and import native Codex conversations into T3 threads

  • Adds a CodexSessionImport service (CodexSessionImport.ts) that lists importable Codex sessions and imports selected ones by creating T3 threads, dispatching thread.history.import commands, and persisting strict resume bindings (requireExistingThread: true).
  • Adds a CodexThreadHistory source (CodexThreadHistory.ts) backed by the Codex app-server child process to list and read native Codex conversations without modifying Codex storage.
  • Extends the orchestration decider to handle the new thread.history.import command, emitting thread.created followed by ordered thread.message-sent events with streaming: false.
  • Adds codexListSessions and codexImportSessions WebSocket RPCs with authorization scopes wired through the server runtime.
  • Adds a CodexSessionImportDialog React component (CodexSessionImportDialog.tsx) in the sidebar project actions panel, allowing users to select and import up to 50 sessions with provider selection, pagination, refresh, and toast feedback.
  • Extends CodexSessionRuntime so sessions with strict bindings do not fall back to starting a new native thread if resume fails — the error is propagated instead.
📊 Macroscope summarized 0ac6907. 20 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e2dbd794-1a35-4b1d-bafc-f87169fa443d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 31, 2026
);
}

function importedMessageId(input: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High Layers/CodexSessionImport.ts:75

importedMessageId omits providerInstanceId, unlike importedThreadId and importCommandId which both include it. Importing the same native Codex thread through two different Codex provider instances produces identical messageId values for two separate T3 threads. Since the projection repository upserts messages globally by messageId, the second import overwrites the first import's projected messages, corrupting the first T3 thread's conversation history. Include providerInstanceId in the importedMessageId key so message IDs are unique per provider instance.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexSessionImport.ts around line 75:

`importedMessageId` omits `providerInstanceId`, unlike `importedThreadId` and `importCommandId` which both include it. Importing the same native Codex thread through two different Codex provider instances produces identical `messageId` values for two separate T3 threads. Since the projection repository upserts messages globally by `messageId`, the second import overwrites the first import's projected messages, corrupting the first T3 thread's conversation history. Include `providerInstanceId` in the `importedMessageId` key so message IDs are unique per provider instance.

Comment on lines +264 to +266
if (truncated) break;
}
threads.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Layers/CodexThreadHistory.ts:264

listThreads stops scanning archived sessions entirely once the unarchived (archived: false) scan exceeds 500 candidates. When there are more than 500 unarchived sessions, no archived sessions are ever queried, even if some archived sessions are newer than unarchived entries that make the final cut. The later global updatedAt sort cannot recover records that were never fetched, so discovery does not return the 500 most recent eligible sessions across both archive states. This happens because the if (truncated) break; at line 264 exits the outer archived loop as soon as truncated is set in the first iteration, skipping the archived: true scan entirely. Consider scanning both archive states and applying the scan limit only across the combined result set, or performing the updatedAt sort and truncation after both scans complete.

          } while (cursor);
        }
+        if (threads.length > CODEX_THREAD_DISCOVERY_MAX_RESULTS) {
+          truncated = true;
+        }
        threads.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexThreadHistory.ts around lines 264-266:

`listThreads` stops scanning archived sessions entirely once the unarchived (`archived: false`) scan exceeds 500 candidates. When there are more than 500 unarchived sessions, no archived sessions are ever queried, even if some archived sessions are newer than unarchived entries that make the final cut. The later global `updatedAt` sort cannot recover records that were never fetched, so discovery does not return the 500 most recent eligible sessions across both archive states. This happens because the `if (truncated) break;` at line 264 exits the outer `archived` loop as soon as `truncated` is set in the first iteration, skipping the `archived: true` scan entirely. Consider scanning both archive states and applying the scan limit only across the combined result set, or performing the `updatedAt` sort and truncation after both scans complete.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

resumeCursor: { threadId: payload.thread.id },

When an imported Codex session with requireExistingThread: true starts successfully, start overwrites the session's resumeCursor with { threadId: providerThreadId }, dropping the requireExistingThread flag. On the next restart, allowResumeFallback evaluates to true (the default), so if the original native thread no longer exists, openCodexThread silently falls back to a fresh thread/start instead of failing — exactly the behavior requireExistingThread was added to prevent. The same drop happens in the thread/started handler. Preserve requireExistingThread when rewriting the resume cursor.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexSessionRuntime.ts around line 920:

When an imported Codex session with `requireExistingThread: true` starts successfully, `start` overwrites the session's `resumeCursor` with `{ threadId: providerThreadId }`, dropping the `requireExistingThread` flag. On the next restart, `allowResumeFallback` evaluates to `true` (the default), so if the original native thread no longer exists, `openCodexThread` silently falls back to a fresh `thread/start` instead of failing — exactly the behavior `requireExistingThread` was added to prevent. The same drop happens in the `thread/started` handler. Preserve `requireExistingThread` when rewriting the resume cursor.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the new Codex session import service and its call sites against the Effect service conventions. Six findings, all in the new/changed Effect service code: a standalone ...Shape interface plus split service/layer modules and a ...Live layer name for a brand-new service, service tags imported as named imports at a service boundary, an error-construction helper, and two error models that carry free-text or constant message/detail instead of structural attributes.

Posted via Macroscope — Effect Service Conventions

Comment on lines +43 to +49
function importError(operation: string, message: string, cause?: unknown): CodexSessionImportError {
return new CodexSessionImportError({
operation,
message,
...(cause === undefined ? {} : { cause }),
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

importError is a helper whose only behavior is new CodexSessionImportError({ ... }), which the conventions ask to avoid so error attributes and cause stay visible at the failure boundary. Consider constructing CodexSessionImportError inline at each failure/mapError site, or replacing this with a static factory on the error class if it starts performing real normalization or pass-through of existing domain errors.

Posted via Macroscope — Effect Service Conventions

Comment on lines +60 to +67
export class CodexSessionImportError extends Schema.TaggedErrorClass<CodexSessionImportError>()(
"CodexSessionImportError",
{
operation: TrimmedNonEmptyString,
message: TrimmedNonEmptyString,
cause: Schema.optional(Schema.Defect()),
},
) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This error stores an unstructured, caller-supplied message as its only real payload and an unconstrained operation string, so message is not derived from structural attributes. Consider constraining operation to Schema.Literals([...]) for the actual stages (resolve-project, resolve-provider, list-sessions, validate-sessions, read-sessions, create-thread, save-binding, ...) and deriving message from those attributes — or splitting the genuinely distinct user-facing failures into separate tagged error classes, since each one currently carries its own caller-visible sentence.

Posted via Macroscope — Effect Service Conventions

Comment on lines +37 to +39
import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
import { CodexSessionImport } from "../Services/CodexSessionImport.ts";
import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At a service boundary these local service modules should be imported as namespaces (as already done for OrchestrationEngine and ProjectionSnapshotQuery above) so the module shape stays visible. Consider import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts" (and likewise for CodexSessionImport and ProviderSessionDirectory), updating the three usage sites to ProviderInstanceRegistry.ProviderInstanceRegistry, ProviderSessionDirectory.ProviderSessionDirectory, and CodexSessionImport.CodexSessionImport.

Posted via Macroscope — Effect Service Conventions

Comment on lines +20 to +34
export interface CodexSessionImportShape {
/** Discover importable sessions whose native workspace matches a T3 project. */
readonly list: (
input: CodexSessionListInput,
) => Effect.Effect<CodexSessionListResult, CodexSessionImportError>;
/** Adopt selected native sessions into the project's T3 thread list. */
readonly import: (
input: CodexSessionImportInput,
) => Effect.Effect<CodexSessionImportResult, CodexSessionImportError>;
}

export class CodexSessionImport extends Context.Service<
CodexSessionImport,
CodexSessionImportShape
>()("t3/provider/Services/CodexSessionImport") {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The service interface should be declared inline in the Context.Service declaration rather than retained as a standalone ...Shape type. Consumers already refer to CodexSessionImport["Service"], so inlining is self-contained.

Suggested change
export interface CodexSessionImportShape {
/** Discover importable sessions whose native workspace matches a T3 project. */
readonly list: (
input: CodexSessionListInput,
) => Effect.Effect<CodexSessionListResult, CodexSessionImportError>;
/** Adopt selected native sessions into the project's T3 thread list. */
readonly import: (
input: CodexSessionImportInput,
) => Effect.Effect<CodexSessionImportResult, CodexSessionImportError>;
}
export class CodexSessionImport extends Context.Service<
CodexSessionImport,
CodexSessionImportShape
>()("t3/provider/Services/CodexSessionImport") {}
export class CodexSessionImport extends Context.Service<
CodexSessionImport,
{
/** Discover importable sessions whose native workspace matches a T3 project. */
readonly list: (
input: CodexSessionListInput,
) => Effect.Effect<CodexSessionListResult, CodexSessionImportError>;
/** Adopt selected native sessions into the project's T3 thread list. */
readonly import: (
input: CodexSessionImportInput,
) => Effect.Effect<CodexSessionImportResult, CodexSessionImportError>;
}
>()("t3/provider/Services/CodexSessionImport") {}

Posted via Macroscope — Effect Service Conventions

return { list, import: importSessions };
});

export const CodexSessionImportLive = Layer.effect(CodexSessionImport, makeCodexSessionImport);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a newly introduced service, the tag, make, and the layer should live in one canonical module, and the layer should be exported as layer. Consider hoisting Services/CodexSessionImport.ts + Layers/CodexSessionImport.ts into apps/server/src/provider/CodexSessionImport.ts with export const make = ... / export const layer = Layer.effect(CodexSessionImport, make), and updating server.ts to use CodexSessionImport.layer.

Posted via Macroscope — Effect Service Conventions

Comment on lines +67 to +75
function toThreadHistoryError(operation: "list" | "read") {
return (cause: CodexErrors.CodexAppServerError) =>
new ProviderThreadHistoryError({
provider: "codex",
operation,
detail: "Codex app-server could not read persisted thread history.",
cause,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a curried alias whose only behavior is constructing ProviderThreadHistoryError, and the detail it passes is a constant that just restates the tag while dropping the context that is available at the failure site (cwd for list, externalThreadId for read). Consider constructing the error where the failure occurs and capturing that stable context (e.g. cwd / externalThreadId) instead of a fixed detail string.

Posted via Macroscope — Effect Service Conventions

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0ac6907. Configure here.

requestedModel,
serviceTier: options.serviceTier,
resumeThreadId: readResumeCursorThreadId(options.resumeCursor),
allowResumeFallback: options.resumeCursor?.requireExistingThread !== true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Strict import flag lost on resume

High Severity

Imported Codex sessions lose their requireExistingThread: true flag from the resumeCursor after the first successful start. This causes subsequent resumes to incorrectly allow fallback to a new thread and impacts their identification as imported.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0ac6907. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

3 blocking correctness issues found. This PR introduces a new feature (Codex session import) with new services, RPC endpoints, orchestration commands, and UI. Multiple high-severity review comments remain unresolved, including potential message ID collisions that could corrupt imported thread history.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant