From cd96603f9af1c5266b428fcfab1a35b328cb533d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 16 Jul 2026 08:55:40 -0700 Subject: [PATCH] fix(codex): show Atlas and ChatGPT sessions in catalog (#109142) * fix(codex): include all interactive session sources * chore: remove release-owned changelog entry --- docs/plugins/codex-harness-runtime.md | 8 ++-- docs/plugins/codex-supervision.md | 15 ++++---- docs/specs/codex-supervision.md | 15 ++++---- extensions/codex/README.md | 2 +- extensions/codex/src/app-server/protocol.ts | 1 + .../src/session-catalog-node-continue.ts | 6 +-- .../codex/src/session-catalog-parsing.ts | 37 +++++++++++++++---- .../codex/src/session-catalog-terminal.ts | 14 ++----- extensions/codex/src/session-catalog.test.ts | 34 ++++++++++++----- extensions/codex/src/session-catalog.ts | 2 - 10 files changed, 83 insertions(+), 51 deletions(-) diff --git a/docs/plugins/codex-harness-runtime.md b/docs/plugins/codex-harness-runtime.md index e6f9a2ff40ce..834b5fe79906 100644 --- a/docs/plugins/codex-harness-runtime.md +++ b/docs/plugins/codex-harness-runtime.md @@ -84,10 +84,10 @@ every later turn remains on that connection with native auth and provider configuration. Disabled supervision or binding/connection drift fails closed rather than switching to the ordinary agent-home harness. -The original CLI or VS Code source remains eligible for both catalogs. The -canonical branch is a native Codex thread, but its source kind is `appServer`; -native clients may filter that source kind, so its appearance in Codex Desktop -is not guaranteed. +The original CLI, VS Code, Atlas, or ChatGPT source remains eligible for both +catalogs. The canonical branch is a native Codex thread, but its source kind is +`appServer`; native clients may filter that source kind, so its appearance in +Codex Desktop is not guaranteed. Active sources cannot start a new branch or be archived; an existing supervised Chat can still be opened. `notLoaded` means activity is unknown, not idle; diff --git a/docs/plugins/codex-supervision.md b/docs/plugins/codex-supervision.md index e38b1c68e2d5..b77c2b73c1a9 100644 --- a/docs/plugins/codex-supervision.md +++ b/docs/plugins/codex-supervision.md @@ -9,8 +9,9 @@ read_when: --- Codex supervision is an opt-in capability of the official `codex` plugin. It -shows non-archived Codex Desktop and CLI source sessions from the Gateway -computer and opted-in paired computers in the normal sessions sidebar and Chat pane. +shows non-archived Codex CLI, VS Code, Atlas, and ChatGPT source sessions from +the Gateway computer and opted-in paired computers in the normal sessions +sidebar and Chat pane. The initial release deliberately keeps ownership narrow: @@ -264,11 +265,11 @@ the source thread or displaying the pending Chat. Starting a distinct canonical harness thread on the first turn lets another Codex process keep owning the source without creating competing rollout writers. -The original CLI or VS Code source remains visible to native clients and the -OpenClaw catalog. The canonical branch is stored as a native Codex thread, but -its source kind is `appServer`; Codex Desktop or another native client may filter -that source kind, so the branch itself is not guaranteed to appear in every -native history view. +The original CLI, VS Code, Atlas, or ChatGPT source remains visible to native +clients and the OpenClaw catalog. The canonical branch is stored as a native +Codex thread, but its source kind is `appServer`; Codex Desktop or another +native client may filter that source kind, so the branch itself is not guaranteed +to appear in every native history view. An active row reported by OpenClaw's App Server cannot start a new branch. Wait for the current turn to finish and refresh the catalog. Codex App Server diff --git a/docs/specs/codex-supervision.md b/docs/specs/codex-supervision.md index ee6426b1acc2..4f3741631b10 100644 --- a/docs/specs/codex-supervision.md +++ b/docs/specs/codex-supervision.md @@ -113,8 +113,9 @@ or assume that daemon implicitly. ## Catalog flow The generic Gateway method `sessions.catalog.list` dispatches to the `codex` -catalog provider, which always requests `archived: false` and -the interactive `cli` and `vscode` source kinds. It combines: +catalog provider, which always requests `archived: false` and lets App Server +apply its interactive-source default: `cli`, `vscode`, Atlas, and ChatGPT. It +combines: 1. Gateway-local `thread/list` results from the supervision App Server, which defaults to managed user-home stdio. @@ -264,11 +265,11 @@ snapshot; it is not the durable continuation thread. Starting a distinct canonical harness thread on the first turn prevents OpenClaw from becoming a competing source writer merely because process-local status failed to see a Desktop-owned turn. The visible-history mirror and pinned snapshot may omit work -that has not yet completed in an active source. The original CLI or VS Code -source remains eligible for both native and OpenClaw catalogs. The canonical -branch remains a native Codex thread in the supervision store, but native clients -may filter its `appServer` source kind, so Codex Desktop visibility is not a -contract. +that has not yet completed in an active source. The original CLI, VS Code, +Atlas, or ChatGPT source remains eligible for both native and OpenClaw catalogs. +The canonical branch remains a native Codex thread in the supervision store, +but native clients may filter its `appServer` source kind, so Codex Desktop +visibility is not a contract. ## Archive behavior diff --git a/extensions/codex/README.md b/extensions/codex/README.md index 9a5db7aa043b..fe2dda6f9880 100644 --- a/extensions/codex/README.md +++ b/extensions/codex/README.md @@ -8,7 +8,7 @@ Install from OpenClaw: openclaw plugins install @openclaw/codex ``` -Use this plugin when you want OpenClaw to run Codex-backed model turns, media understanding, and prompt overlays through the Codex app-server harness, or to browse non-archived Codex Desktop and CLI sessions and paginated transcripts across paired computers. +Use this plugin when you want OpenClaw to run Codex-backed model turns, media understanding, and prompt overlays through the Codex app-server harness, or to browse non-archived Codex CLI, VS Code, Atlas, and ChatGPT sessions and paginated transcripts across paired computers. Guided onboarding attempts to install and enable supervision after it detects a native Codex installation and the selected inference backend passes its live check; Codex does not need to be the primary backend. Supervision activates when that opportunistic plugin setup succeeds. App Server availability is checked when supervision connects. An explicit Codex plugin disable, plugin-policy block, or `supervision.enabled: false` prevents opportunistic enablement. Manual setups enable `plugins.entries.codex.config.supervision.enabled`. Without explicit App Server connection settings, supervision uses a managed user-home stdio connection; explicit `appServer` settings are honored. diff --git a/extensions/codex/src/app-server/protocol.ts b/extensions/codex/src/app-server/protocol.ts index 476b627c5ef6..ed6cbb5248c5 100644 --- a/extensions/codex/src/app-server/protocol.ts +++ b/extensions/codex/src/app-server/protocol.ts @@ -180,6 +180,7 @@ export type CodexThreadForkParams = JsonObject & { export type CodexThreadForkResponse = CodexThreadStartResponse; export const CODEX_INTERACTIVE_THREAD_SOURCE_KINDS = ["cli", "vscode"] as const; +export const CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES = ["atlas", "chatgpt"] as const; type CodexThreadSourceKind = | (typeof CODEX_INTERACTIVE_THREAD_SOURCE_KINDS)[number] diff --git a/extensions/codex/src/session-catalog-node-continue.ts b/extensions/codex/src/session-catalog-node-continue.ts index b1ff33e46ec1..0bd5f9fecdf7 100644 --- a/extensions/codex/src/session-catalog-node-continue.ts +++ b/extensions/codex/src/session-catalog-node-continue.ts @@ -188,12 +188,10 @@ function requireContinuableNodeRecord(record: CodexSessionCatalogSession): void throw new CatalogParamsError("Codex session is archived on the paired node"); } if (!isInteractiveThreadSource(record.source)) { - throw new CatalogParamsError( - "Codex session is not a non-archived interactive CLI or VS Code session", - ); + throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session"); } if (record.status === "idle" || record.status === "notLoaded") { - // The node App Server is a passive catalog reader, so stored CLI/VS Code + // The node App Server is a passive catalog reader, so stored native Codex // sessions normally report notLoaded. Node resume serializes OpenClaw turns. return; } diff --git a/extensions/codex/src/session-catalog-parsing.ts b/extensions/codex/src/session-catalog-parsing.ts index bce8e4910cf8..ed2c1c21437d 100644 --- a/extensions/codex/src/session-catalog-parsing.ts +++ b/extensions/codex/src/session-catalog-parsing.ts @@ -1,7 +1,10 @@ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { CodexThread, CodexThreadTurnsListResponse } from "./app-server/protocol.js"; -import { CODEX_INTERACTIVE_THREAD_SOURCE_KINDS } from "./app-server/protocol.js"; +import { + CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES, + CODEX_INTERACTIVE_THREAD_SOURCE_KINDS, +} from "./app-server/protocol.js"; import type { CodexSessionCatalogError, CodexSessionCatalogPage, @@ -65,20 +68,40 @@ export function boundedCatalogString( return overflow === "truncate" ? truncateUtf16Safe(normalized, maxLength) : undefined; } -type CodexInteractiveThreadSourceKind = (typeof CODEX_INTERACTIVE_THREAD_SOURCE_KINDS)[number]; +type CodexInteractiveThreadSource = + | (typeof CODEX_INTERACTIVE_THREAD_SOURCE_KINDS)[number] + | (typeof CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES)[number]; -export function isInteractiveThreadSource( +function normalizeInteractiveThreadSource( source: unknown, -): source is CodexInteractiveThreadSourceKind { - return CODEX_INTERACTIVE_THREAD_SOURCE_KINDS.some((kind) => kind === source); +): CodexInteractiveThreadSource | undefined { + if ( + CODEX_INTERACTIVE_THREAD_SOURCE_KINDS.some((kind) => kind === source) || + CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES.some((kind) => kind === source) + ) { + return source as CodexInteractiveThreadSource; + } + if ( + isRecord(source) && + CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES.some((kind) => kind === source.custom) + ) { + return source.custom as (typeof CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES)[number]; + } + return undefined; +} + +export function isInteractiveThreadSource(source: unknown): boolean { + return normalizeInteractiveThreadSource(source) !== undefined; } export function toCatalogSession( thread: CodexThread, archived: boolean, ): CodexSessionCatalogSession | undefined { - const source = thread.source; - if (!isInteractiveThreadSource(source)) { + // Codex models Atlas and ChatGPT as custom sources but includes both in its + // interactive default. Normalize those objects for the string-only catalog. + const source = normalizeInteractiveThreadSource(thread.source); + if (!source) { return undefined; } const record = thread as CodexThread & Record; diff --git a/extensions/codex/src/session-catalog-terminal.ts b/extensions/codex/src/session-catalog-terminal.ts index 388a977b46b3..57a10dd281f6 100644 --- a/extensions/codex/src/session-catalog-terminal.ts +++ b/extensions/codex/src/session-catalog-terminal.ts @@ -67,18 +67,14 @@ export async function requireCatalogEligibleThread( }); const candidate = page.sessions.find((session) => session.threadId === threadId); if (candidate) { - if (candidate.source === "cli" || candidate.source === "vscode") { + if (isInteractiveThreadSource(candidate.source)) { return candidate; } - throw new CatalogParamsError( - "Codex session is not a non-archived interactive CLI or VS Code session", - ); + throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session"); } const nextCursor = page.nextCursor?.trim(); if (!nextCursor) { - throw new CatalogParamsError( - "Codex session is not a non-archived interactive CLI or VS Code session", - ); + throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session"); } if (seenCursors.has(nextCursor)) { throw new CatalogParamsError("Codex session eligibility could not be verified"); @@ -177,9 +173,7 @@ async function resolveNodeCatalogEligibleThread(params: { seenCursors.add(nextCursor); cursor = nextCursor; } - throw new CatalogParamsError( - "Codex session is not a non-archived interactive CLI or VS Code session", - ); + throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session"); } export async function openCodexCatalogTerminal(params: { diff --git a/extensions/codex/src/session-catalog.test.ts b/extensions/codex/src/session-catalog.test.ts index 27e37e36e5b1..5182d5b79400 100644 --- a/extensions/codex/src/session-catalog.test.ts +++ b/extensions/codex/src/session-catalog.test.ts @@ -449,7 +449,6 @@ describe("Codex supervision catalog", () => { modelProviders: [], sortKey: "recency_at", sortDirection: "desc", - sourceKinds: ["cli", "vscode"], cwd: "/workspace/one", }, { @@ -643,11 +642,13 @@ describe("Codex supervision catalog", () => { }, ); - it("omits noninteractive sources when App Server ignores the requested source kinds", async () => { + it("keeps every Codex interactive source while omitting other custom sources", async () => { commandRpcMocks.codexControlRequest.mockResolvedValue({ data: [ idleThread({ id: "cli", source: "cli" }), idleThread({ id: "vscode", source: "vscode" }), + idleThread({ id: "atlas", source: { custom: "atlas" } }), + idleThread({ id: "chatgpt", source: { custom: "chatgpt" } }), idleThread({ id: "exec", source: "exec" }), idleThread({ id: "app-server", source: "appServer" }), idleThread({ id: "subagent", source: { subAgent: "review" } }), @@ -663,8 +664,18 @@ describe("Codex supervision catalog", () => { const page = await control.listPage({}); - expect(page.sessions.map((session) => session.threadId)).toEqual(["cli", "vscode"]); - expect(page.sessions.map((session) => session.source)).toEqual(["cli", "vscode"]); + expect(page.sessions.map((session) => session.threadId)).toEqual([ + "cli", + "vscode", + "atlas", + "chatgpt", + ]); + expect(page.sessions.map((session) => session.source)).toEqual([ + "cli", + "vscode", + "atlas", + "chatgpt", + ]); }); it("keeps takeover forking out of the passive catalog control", async () => { @@ -1057,7 +1068,7 @@ describe("Codex supervision catalog", () => { { threadId, status: "idle", - source: "cli", + source: "atlas", cwd: "/node/catalog/cwd", archived: false, }, @@ -2415,9 +2426,9 @@ describe("Codex supervision actions", () => { control, threadId: "thread-1", }), - ).rejects.toThrow("not a non-archived interactive CLI or VS Code session"); + ).rejects.toThrow("not a non-archived interactive Codex session"); await expect(archiveTestSession({ control })).rejects.toThrow( - "not a non-archived interactive CLI or VS Code session", + "not a non-archived interactive Codex session", ); expect(control.readThread).not.toHaveBeenCalled(); expect(createSessionEntry).not.toHaveBeenCalled(); @@ -2448,9 +2459,9 @@ describe("Codex supervision actions", () => { control, threadId: "thread-1", }), - ).rejects.toThrow("not a non-archived interactive CLI or VS Code session"); + ).rejects.toThrow("not a non-archived interactive Codex session"); await expect(archiveTestSession({ control })).rejects.toThrow( - "not a non-archived interactive CLI or VS Code session", + "not a non-archived interactive Codex session", ); expect(control.readThread).not.toHaveBeenCalled(); }); @@ -2841,6 +2852,7 @@ describe("Codex supervision actions", () => { const sourceByNode = new Map([ ["ready-cli", { status: "idle", source: "cli" }], ["ready-vscode", { status: "notLoaded", source: "vscode" }], + ["ready-atlas", { status: "notLoaded", source: "atlas" }], ["missing-run", { status: "idle", source: "cli" }], ["active", { status: "active", source: "cli" }], ["noninteractive", { status: "idle", source: "exec" }], @@ -2898,6 +2910,10 @@ describe("Codex supervision actions", () => { canContinue: true, canArchive: false, }); + expect(sessionByHost.get("node:ready-atlas")).toMatchObject({ + canContinue: true, + canArchive: false, + }); expect(sessionByHost.get("node:missing-run")).toMatchObject({ canContinue: false }); expect(sessionByHost.get("node:active")).toMatchObject({ canContinue: false }); expect(sessionByHost.get("node:noninteractive")).toMatchObject({ canContinue: false }); diff --git a/extensions/codex/src/session-catalog.ts b/extensions/codex/src/session-catalog.ts index 43bad3f9873f..f076f586ced4 100644 --- a/extensions/codex/src/session-catalog.ts +++ b/extensions/codex/src/session-catalog.ts @@ -23,7 +23,6 @@ import type { CodexThreadTurnsListParams, CodexThreadTurnsListResponse, } from "./app-server/protocol.js"; -import { CODEX_INTERACTIVE_THREAD_SOURCE_KINDS } from "./app-server/protocol.js"; import { requestCodexAppServerClientJson } from "./app-server/request.js"; import { reclaimCurrentCodexSessionGeneration, @@ -167,7 +166,6 @@ function createCodexSessionCatalogControlFromRequests(params: { modelProviders: [], sortKey: "recency_at", sortDirection: "desc", - sourceKinds: [...CODEX_INTERACTIVE_THREAD_SOURCE_KINDS], ...(cwd ? { cwd } : {}), ...(cursor ? { cursor } : {}), },