fix(codex): show Atlas and ChatGPT sessions in catalog (#109142)

* fix(codex): include all interactive session sources

* chore: remove release-owned changelog entry
This commit is contained in:
Peter Steinberger
2026-07-16 08:55:40 -07:00
committed by GitHub
parent 65bdd1bf08
commit cd96603f9a
10 changed files with 83 additions and 51 deletions
+4 -4
View File
@@ -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;
+8 -7
View File
@@ -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
+8 -7
View File
@@ -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
+1 -1
View File
@@ -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.
@@ -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]
@@ -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;
}
@@ -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<string, unknown>;
@@ -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: {
+25 -9
View File
@@ -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 });
-2
View File
@@ -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 } : {}),
},