@nyssance/codex-acp-v2 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/index.js +174 -15
- package/docs/protocol.md +798 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.12 — 2026-10-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `_codex/plugin_set_enabled` enables or disables local and remote plugins,
|
|
8
|
+
advertised through `capabilities._meta.codex.pluginEnable`. Successful changes
|
|
9
|
+
emit `_codex/skills_changed` so clients can refresh the effective catalog.
|
|
10
|
+
- Remote plugin changes use ChatGPT plugin-service, reconcile installed plugins,
|
|
11
|
+
and verify the resulting state. Expired credentials are refreshed once without
|
|
12
|
+
exposing credentials over ACP; local changes report configuration overrides.
|
|
13
|
+
|
|
14
|
+
## 0.7.11 — 2026-10-12
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `_codex/session_list` returns all non-archived sessions across projects when called
|
|
19
|
+
without params, walking native pages and deduplicating session IDs. Requests with
|
|
20
|
+
defined params retain the filtering and pagination behavior of `session/list`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Suppress duplicate review results in live updates and full-turn history when the
|
|
25
|
+
review exit repeats the assistant's answer, while preserving standalone or different
|
|
26
|
+
review results regardless of native event order.
|
|
27
|
+
|
|
3
28
|
## 0.7.10 — 2026-10-11
|
|
4
29
|
|
|
5
30
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -22168,7 +22168,7 @@ var package_default = {
|
|
|
22168
22168
|
publishConfig: {
|
|
22169
22169
|
access: "public"
|
|
22170
22170
|
},
|
|
22171
|
-
version: "0.7.
|
|
22171
|
+
version: "0.7.12",
|
|
22172
22172
|
description: "Native ACP v2 agent for the OpenAI Codex app-server",
|
|
22173
22173
|
packageManager: "bun@1.4.0",
|
|
22174
22174
|
engines: {
|
|
@@ -22182,7 +22182,8 @@ var package_default = {
|
|
|
22182
22182
|
"README.md",
|
|
22183
22183
|
"CHANGELOG.md",
|
|
22184
22184
|
"LICENSE",
|
|
22185
|
-
"package.json"
|
|
22185
|
+
"package.json",
|
|
22186
|
+
"docs/protocol.md"
|
|
22186
22187
|
],
|
|
22187
22188
|
scripts: {
|
|
22188
22189
|
prepublishOnly: "bun run build",
|
|
@@ -24273,6 +24274,12 @@ function itemSnapshot(item, command) {
|
|
|
24273
24274
|
return [];
|
|
24274
24275
|
}
|
|
24275
24276
|
}
|
|
24277
|
+
function isDuplicateReview(item, items) {
|
|
24278
|
+
if (item.type !== "exitedReviewMode")
|
|
24279
|
+
return false;
|
|
24280
|
+
const text = item.review.trim();
|
|
24281
|
+
return text.length > 0 && Array.from(items).some((candidate) => candidate.type === "agentMessage" && candidate.phase !== "commentary" && candidate.text.trim() === text);
|
|
24282
|
+
}
|
|
24276
24283
|
|
|
24277
24284
|
// src/bridge/turnMetadata.ts
|
|
24278
24285
|
function withTurnId(update, turnId, startedAt) {
|
|
@@ -24314,6 +24321,8 @@ class EventBridge {
|
|
|
24314
24321
|
turnPrompt = null;
|
|
24315
24322
|
openToolCalls = new Set;
|
|
24316
24323
|
announcedTools = new Set;
|
|
24324
|
+
pendingReviews = new Map;
|
|
24325
|
+
reviewMessages = new Map;
|
|
24317
24326
|
messagePhases = new Map;
|
|
24318
24327
|
promptMeta = new Map;
|
|
24319
24328
|
reasoningWithDeltas = new Set;
|
|
@@ -24377,6 +24386,8 @@ class EventBridge {
|
|
|
24377
24386
|
this.openToolCalls.clear();
|
|
24378
24387
|
this.announcedTools.clear();
|
|
24379
24388
|
this.messagePhases.clear();
|
|
24389
|
+
this.reviewMessages.clear();
|
|
24390
|
+
this.pendingReviews.clear();
|
|
24380
24391
|
this.reasoningWithDeltas.clear();
|
|
24381
24392
|
this.terminalItems.clear();
|
|
24382
24393
|
this.imageGenerations.clear();
|
|
@@ -24399,6 +24410,8 @@ class EventBridge {
|
|
|
24399
24410
|
for (const item of items) {
|
|
24400
24411
|
if (item.type === "userMessage")
|
|
24401
24412
|
this.unresolvedUserItems.add(item.id);
|
|
24413
|
+
if (item.type === "agentMessage")
|
|
24414
|
+
this.reviewMessages.set(item.id, item);
|
|
24402
24415
|
if (!replay && itemSnapshot(item).some((update) => update.sessionUpdate === "tool_call_update")) {
|
|
24403
24416
|
this.unannouncedItems.set(item.id, { item, turnId });
|
|
24404
24417
|
}
|
|
@@ -24411,8 +24424,12 @@ class EventBridge {
|
|
|
24411
24424
|
if (receipt)
|
|
24412
24425
|
this.restoredCommands.set(item.id, receipt);
|
|
24413
24426
|
this.unresolvedUserItems.delete(item.id);
|
|
24414
|
-
if (!replay)
|
|
24427
|
+
if (!replay || isDuplicateReview(item, this.reviewMessages.values()))
|
|
24415
24428
|
continue;
|
|
24429
|
+
if (item.type === "exitedReviewMode") {
|
|
24430
|
+
this.pendingReviews.set(item.id, { item, turnId });
|
|
24431
|
+
continue;
|
|
24432
|
+
}
|
|
24416
24433
|
if (item.type === "commandExecution" && item.status === "inProgress" && usesTerminal(item))
|
|
24417
24434
|
this.terminalItems.add(item.id);
|
|
24418
24435
|
for (const update of itemSnapshot(item, receipt ?? undefined)) {
|
|
@@ -24532,6 +24549,7 @@ class EventBridge {
|
|
|
24532
24549
|
}
|
|
24533
24550
|
}
|
|
24534
24551
|
async finishOpenToolCalls(status) {
|
|
24552
|
+
await this.flushReviews();
|
|
24535
24553
|
this.unannouncedItems.clear();
|
|
24536
24554
|
const ids = [...this.openToolCalls];
|
|
24537
24555
|
const terminals = [...this.terminalItems];
|
|
@@ -24556,6 +24574,18 @@ class EventBridge {
|
|
|
24556
24574
|
}
|
|
24557
24575
|
async flush() {
|
|
24558
24576
|
await this.flushPlans();
|
|
24577
|
+
await this.flushReviews();
|
|
24578
|
+
}
|
|
24579
|
+
async flushReviews() {
|
|
24580
|
+
const pending = [...this.pendingReviews.values()];
|
|
24581
|
+
this.pendingReviews.clear();
|
|
24582
|
+
for (const { item, turnId } of pending) {
|
|
24583
|
+
if (isDuplicateReview(item, this.reviewMessages.values()))
|
|
24584
|
+
continue;
|
|
24585
|
+
for (const update of itemSnapshot(item)) {
|
|
24586
|
+
await this.client.update(this.withTiming(withTurnId(update, turnId, this.turnTime?.startedAt), turnId));
|
|
24587
|
+
}
|
|
24588
|
+
}
|
|
24559
24589
|
}
|
|
24560
24590
|
dispose() {
|
|
24561
24591
|
this.beginTurn();
|
|
@@ -24578,7 +24608,7 @@ class EventBridge {
|
|
|
24578
24608
|
case "item/started":
|
|
24579
24609
|
return this.itemStarted(notification.params.item);
|
|
24580
24610
|
case "item/completed":
|
|
24581
|
-
return this.itemCompleted(notification.params.item);
|
|
24611
|
+
return this.itemCompleted(notification.params.item, notification.params.turnId);
|
|
24582
24612
|
case "item/commandExecution/outputDelta": {
|
|
24583
24613
|
const { itemId, delta } = notification.params;
|
|
24584
24614
|
return this.terminalItems.has(itemId) && delta.length > 0 ? [terminalOutputChunk(itemId, delta)] : [];
|
|
@@ -24792,9 +24822,10 @@ ${details}` : summary)];
|
|
|
24792
24822
|
return [];
|
|
24793
24823
|
}
|
|
24794
24824
|
}
|
|
24795
|
-
async itemCompleted(item) {
|
|
24825
|
+
async itemCompleted(item, turnId) {
|
|
24796
24826
|
switch (item.type) {
|
|
24797
24827
|
case "agentMessage":
|
|
24828
|
+
this.reviewMessages.set(item.id, item);
|
|
24798
24829
|
this.messagePhases.delete(item.id);
|
|
24799
24830
|
return itemSnapshot(item);
|
|
24800
24831
|
case "reasoning": {
|
|
@@ -24834,8 +24865,8 @@ ${details}` : summary)];
|
|
|
24834
24865
|
case "plan":
|
|
24835
24866
|
return await this.planCompleted(item);
|
|
24836
24867
|
case "exitedReviewMode": {
|
|
24837
|
-
|
|
24838
|
-
return
|
|
24868
|
+
this.pendingReviews.set(item.id, { item, turnId });
|
|
24869
|
+
return [];
|
|
24839
24870
|
}
|
|
24840
24871
|
case "userMessage":
|
|
24841
24872
|
case "hookPrompt":
|
|
@@ -25379,6 +25410,15 @@ class AppServerClient {
|
|
|
25379
25410
|
skillsConfigWrite(params) {
|
|
25380
25411
|
return this.send({ method: "skills/config/write", params });
|
|
25381
25412
|
}
|
|
25413
|
+
configValueWrite(params) {
|
|
25414
|
+
return this.send({ method: "config/value/write", params });
|
|
25415
|
+
}
|
|
25416
|
+
getAuthStatus(params) {
|
|
25417
|
+
return this.send({ method: "getAuthStatus", params });
|
|
25418
|
+
}
|
|
25419
|
+
pluginReconcile(params) {
|
|
25420
|
+
return this.send({ method: "plugin/reconcile", params });
|
|
25421
|
+
}
|
|
25382
25422
|
pluginList(params) {
|
|
25383
25423
|
return this.send({ method: "plugin/list", params });
|
|
25384
25424
|
}
|
|
@@ -25448,6 +25488,44 @@ function threadIdOf(notification) {
|
|
|
25448
25488
|
return params && typeof params.threadId === "string" ? params.threadId : null;
|
|
25449
25489
|
}
|
|
25450
25490
|
|
|
25491
|
+
// src/codex/remotePluginEnablement.ts
|
|
25492
|
+
async function setRemotePluginEnabled(codex, remotePluginId, enabled) {
|
|
25493
|
+
const account = await codex.accountRead({ refreshToken: false });
|
|
25494
|
+
const routing = account.workspaceRouting;
|
|
25495
|
+
if (account.account?.type !== "chatgpt" || !routing)
|
|
25496
|
+
throw new Error("Sign in to ChatGPT to change remote plugins");
|
|
25497
|
+
const origin = new URL(routing.backendOrigin);
|
|
25498
|
+
if (origin.protocol !== "https:" || origin.hostname !== "chatgpt.com" && !origin.hostname.endsWith(".chatgpt.com")) {
|
|
25499
|
+
throw new Error("Unsupported ChatGPT backend for remote plugins");
|
|
25500
|
+
}
|
|
25501
|
+
const url2 = new URL(`/backend-api/ps/plugins/${encodeURIComponent(remotePluginId)}/${enabled ? "enable" : "disable"}`, origin);
|
|
25502
|
+
for (const refreshToken of [false, true]) {
|
|
25503
|
+
const auth = await codex.getAuthStatus({ includeToken: true, refreshToken });
|
|
25504
|
+
if (auth.authMethod !== "chatgpt" && auth.authMethod !== "chatgptAuthTokens" || !auth.authToken) {
|
|
25505
|
+
throw new Error("ChatGPT credentials are unavailable; sign in again to change remote plugins");
|
|
25506
|
+
}
|
|
25507
|
+
const response = await fetch(url2, {
|
|
25508
|
+
method: "POST",
|
|
25509
|
+
headers: { Authorization: `Bearer ${auth.authToken}`, "ChatGPT-Account-Id": routing.chatgptAccountId, "OpenAI-Product-SKU": "codex" },
|
|
25510
|
+
redirect: "error",
|
|
25511
|
+
signal: AbortSignal.any([codex.disconnectSignal, AbortSignal.timeout(30000)])
|
|
25512
|
+
});
|
|
25513
|
+
if (response.status === 401 && !refreshToken) {
|
|
25514
|
+
await response.body?.cancel();
|
|
25515
|
+
continue;
|
|
25516
|
+
}
|
|
25517
|
+
if (!response.ok) {
|
|
25518
|
+
await response.body?.cancel();
|
|
25519
|
+
throw new Error(`Remote plugin ${enabled ? "enable" : "disable"} failed (HTTP ${response.status})`);
|
|
25520
|
+
}
|
|
25521
|
+
const result = await response.json();
|
|
25522
|
+
if (typeof result !== "object" || result === null || !("id" in result) || result.id !== remotePluginId || !("enabled" in result) || result.enabled !== enabled) {
|
|
25523
|
+
throw new Error("Remote plugin service did not confirm the requested enabled state");
|
|
25524
|
+
}
|
|
25525
|
+
return;
|
|
25526
|
+
}
|
|
25527
|
+
}
|
|
25528
|
+
|
|
25451
25529
|
// src/codex/modes.ts
|
|
25452
25530
|
var MODE_CONFIG_ID = "mode";
|
|
25453
25531
|
var workspaceWrite = {
|
|
@@ -27959,6 +28037,8 @@ function historyUpdates(turns, commandReceipts = new Map) {
|
|
|
27959
28037
|
for (const turn of turns) {
|
|
27960
28038
|
const openingUser = turn.items.find((item) => item.type === "userMessage");
|
|
27961
28039
|
for (const item of turn.items) {
|
|
28040
|
+
if (isDuplicateReview(item, turn.items))
|
|
28041
|
+
continue;
|
|
27962
28042
|
const command = item.type === "userMessage" ? commandReceipts.get(item.id) : undefined;
|
|
27963
28043
|
updates.push(...itemSnapshot(item, command).map((update) => {
|
|
27964
28044
|
const replay = withTurnId(update, turn.id, turn.startedAt);
|
|
@@ -28222,7 +28302,7 @@ async function readSessionHistory(codex, raw, signal, commandReceipts) {
|
|
|
28222
28302
|
if (view === "full")
|
|
28223
28303
|
await read.call(() => assertCommandHistory(codex, start.thread, commandReceipts, signal));
|
|
28224
28304
|
const turns = await read.call(() => Promise.all(page.data.map(async (turn) => {
|
|
28225
|
-
const projections = view === "full" ? await Promise.all(turn.items.map(async (item) => projectHistoryItem(item, turn.id, turn.startedAt, item.type === "userMessage" ? await commandReceipts?.read(item.id) : null))) : [];
|
|
28305
|
+
const projections = view === "full" ? await Promise.all(turn.items.filter((item) => !isDuplicateReview(item, turn.items)).map(async (item) => projectHistoryItem(item, turn.id, turn.startedAt, item.type === "userMessage" ? await commandReceipts?.read(item.id) : null))) : [];
|
|
28226
28306
|
return {
|
|
28227
28307
|
turnId: turn.id,
|
|
28228
28308
|
status: turn.status,
|
|
@@ -28501,7 +28581,7 @@ class CodexAgent {
|
|
|
28501
28581
|
additionalDirectories: {}
|
|
28502
28582
|
},
|
|
28503
28583
|
providers: {},
|
|
28504
|
-
_meta: { alwith: { tools: { version: 1 }, turns: { version: 2 } }, codex: { forkAtTurn: true, sessionLineage: true, archive: true, sessionHistory: { version: 2, items: true, modes: ["browse", "export"] }, seedHistory: true, skills: true, plugins: true, providerCatalog: true, rename: true, account: true, fuzzyFileSearch: true } }
|
|
28584
|
+
_meta: { alwith: { tools: { version: 1 }, turns: { version: 2 } }, codex: { forkAtTurn: true, sessionLineage: true, archive: true, sessionList: { version: 1 }, sessionHistory: { version: 2, items: true, modes: ["browse", "export"] }, seedHistory: true, skills: true, plugins: true, pluginEnable: true, providerCatalog: true, rename: true, account: true, fuzzyFileSearch: true } }
|
|
28505
28585
|
},
|
|
28506
28586
|
authMethods: authMethods(this.capabilities, this.env)
|
|
28507
28587
|
};
|
|
@@ -29067,16 +29147,41 @@ class CodexAgent {
|
|
|
29067
29147
|
}
|
|
29068
29148
|
async listSessions(params) {
|
|
29069
29149
|
this.requireInitialized("session/list");
|
|
29150
|
+
return this.readSessionList(params, false);
|
|
29151
|
+
}
|
|
29152
|
+
async sessionList(params = {}) {
|
|
29153
|
+
this.requireInitialized("_codex/session_list");
|
|
29154
|
+
if (Object.values(params).some((value) => value !== undefined))
|
|
29155
|
+
return this.listSessions(params);
|
|
29156
|
+
return this.readSessionList({}, true);
|
|
29157
|
+
}
|
|
29158
|
+
async readSessionList(params, all) {
|
|
29070
29159
|
const cwd = params.cwd?.trim() || null;
|
|
29071
29160
|
const archived = archivedFilter(params._meta);
|
|
29072
29161
|
const list = (cursor) => this.withCodex(() => this.codex.threadList({
|
|
29073
29162
|
cursor,
|
|
29163
|
+
...all ? { limit: 100 } : {},
|
|
29074
29164
|
...cwd ? { cwd } : {},
|
|
29075
29165
|
sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
|
|
29076
29166
|
modelProviders: [],
|
|
29077
29167
|
...archived ? { archived: true } : {}
|
|
29078
29168
|
}));
|
|
29079
29169
|
const response = await list(params.cursor ?? null);
|
|
29170
|
+
if (all) {
|
|
29171
|
+
const threads = new Map(response.data.map((thread) => [thread.id, thread]));
|
|
29172
|
+
const cursors = new Set;
|
|
29173
|
+
while (response.nextCursor !== null) {
|
|
29174
|
+
const cursor = response.nextCursor;
|
|
29175
|
+
if (cursors.has(cursor))
|
|
29176
|
+
throw RequestError.internalError(undefined, "Codex thread list repeated a cursor");
|
|
29177
|
+
cursors.add(cursor);
|
|
29178
|
+
const page = await list(cursor);
|
|
29179
|
+
for (const thread of page.data)
|
|
29180
|
+
threads.set(thread.id, thread);
|
|
29181
|
+
response.nextCursor = page.nextCursor;
|
|
29182
|
+
}
|
|
29183
|
+
response.data = [...threads.values()];
|
|
29184
|
+
}
|
|
29080
29185
|
const home = this.codexHome;
|
|
29081
29186
|
if (home === null)
|
|
29082
29187
|
throw RequestError.internalError(undefined, "Codex home is unavailable after initialize");
|
|
@@ -29100,16 +29205,18 @@ class CodexAgent {
|
|
|
29100
29205
|
}
|
|
29101
29206
|
response.data.push(...missing2.values());
|
|
29102
29207
|
}
|
|
29103
|
-
|
|
29104
|
-
|
|
29208
|
+
const sessions = [];
|
|
29209
|
+
const batchSize = all ? 8 : response.data.length;
|
|
29210
|
+
for (let offset = 0;offset < response.data.length; offset += batchSize) {
|
|
29211
|
+
sessions.push(...await Promise.all(response.data.slice(offset, offset + batchSize).map(async (thread) => ({
|
|
29105
29212
|
sessionId: thread.id,
|
|
29106
29213
|
cwd: thread.cwd,
|
|
29107
29214
|
title: !thread.name?.trim() && /^(?:\[@|# Files mentioned by the user:|<send_user_message_question_reply>)/.test(thread.preview.trimStart()) ? historyTitle(thread, (await this.turnPage(thread.id, null)).data) : thread.name?.trim() || thread.preview.trim() || null,
|
|
29108
29215
|
updatedAt: new Date(thread.updatedAt * 1000).toISOString(),
|
|
29109
29216
|
_meta: { codex: { archived, ...threadLineage(thread) } }
|
|
29110
|
-
})))
|
|
29111
|
-
|
|
29112
|
-
};
|
|
29217
|
+
}))));
|
|
29218
|
+
}
|
|
29219
|
+
return all ? { sessions } : { sessions, nextCursor: response.nextCursor };
|
|
29113
29220
|
}
|
|
29114
29221
|
async closeSession(params) {
|
|
29115
29222
|
return await this.admission(() => this.closeSessionInternal(params), params.sessionId);
|
|
@@ -29173,6 +29280,31 @@ class CodexAgent {
|
|
|
29173
29280
|
this.notifySkillsChanged();
|
|
29174
29281
|
return response;
|
|
29175
29282
|
}
|
|
29283
|
+
async pluginSetEnabled(params) {
|
|
29284
|
+
this.requireInitialized("_codex/plugin_set_enabled");
|
|
29285
|
+
if (params.remotePluginId !== undefined) {
|
|
29286
|
+
await this.withCodex(async () => {
|
|
29287
|
+
await setRemotePluginEnabled(this.codex, params.remotePluginId, params.enabled);
|
|
29288
|
+
await this.codex.pluginReconcile({ reason: "plugin-enabled-changed" });
|
|
29289
|
+
const installed = await this.codex.pluginInstalled({});
|
|
29290
|
+
const plugin = installed.marketplaces.flatMap((market) => market.plugins).find((plugin2) => plugin2.remotePluginId === params.remotePluginId);
|
|
29291
|
+
if (!plugin || plugin.enabled !== params.enabled)
|
|
29292
|
+
throw new Error("Remote plugin state could not be synchronized; refresh the plugin catalog");
|
|
29293
|
+
});
|
|
29294
|
+
this.notifySkillsChanged();
|
|
29295
|
+
return { enabled: params.enabled };
|
|
29296
|
+
}
|
|
29297
|
+
const response = await this.withCodex(() => this.codex.configValueWrite({
|
|
29298
|
+
keyPath: `plugins.${JSON.stringify(params.pluginId)}.enabled`,
|
|
29299
|
+
value: params.enabled,
|
|
29300
|
+
mergeStrategy: "upsert"
|
|
29301
|
+
}));
|
|
29302
|
+
this.notifySkillsChanged();
|
|
29303
|
+
if (response.status === "okOverridden") {
|
|
29304
|
+
throw RequestError.invalidRequest(undefined, "Plugin enablement is overridden by a higher-priority configuration");
|
|
29305
|
+
}
|
|
29306
|
+
return { enabled: params.enabled };
|
|
29307
|
+
}
|
|
29176
29308
|
async pluginList(params) {
|
|
29177
29309
|
this.requireInitialized("_codex/plugin_list");
|
|
29178
29310
|
return await this.withCodex(() => this.codex.pluginList(params));
|
|
@@ -29960,6 +30092,20 @@ function interruptedTurn(threadId) {
|
|
|
29960
30092
|
turn: { id: "", items: [], itemsView: "notLoaded", status: "interrupted", error: null, startedAt: null, completedAt: null, durationMs: null }
|
|
29961
30093
|
};
|
|
29962
30094
|
}
|
|
30095
|
+
function parseSessionListParams(raw) {
|
|
30096
|
+
const params = objectParams()(raw);
|
|
30097
|
+
for (const key of ["cwd", "cursor"]) {
|
|
30098
|
+
const value = params[key];
|
|
30099
|
+
if (value !== undefined && value !== null && typeof value !== "string") {
|
|
30100
|
+
throw RequestError.invalidParams({ [key]: value }, `${key} must be a string when given`);
|
|
30101
|
+
}
|
|
30102
|
+
}
|
|
30103
|
+
const meta = params["_meta"];
|
|
30104
|
+
if (meta !== undefined && meta !== null && (typeof meta !== "object" || Array.isArray(meta))) {
|
|
30105
|
+
throw RequestError.invalidParams({ _meta: meta }, "_meta must be an object when given");
|
|
30106
|
+
}
|
|
30107
|
+
return params;
|
|
30108
|
+
}
|
|
29963
30109
|
function parseSessionRenameParams(raw) {
|
|
29964
30110
|
const params = objectParams()(raw);
|
|
29965
30111
|
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
@@ -30095,6 +30241,19 @@ function forkTurnId(meta) {
|
|
|
30095
30241
|
}
|
|
30096
30242
|
return value;
|
|
30097
30243
|
}
|
|
30244
|
+
function parsePluginSetEnabledParams(value) {
|
|
30245
|
+
if (typeof value !== "object" || value === null)
|
|
30246
|
+
throw RequestError.invalidParams(undefined, "Expected pluginId and enabled");
|
|
30247
|
+
const params = value;
|
|
30248
|
+
if (typeof params["pluginId"] !== "string" || !params["pluginId"].trim() || typeof params["enabled"] !== "boolean") {
|
|
30249
|
+
throw RequestError.invalidParams(undefined, "Expected a non-empty pluginId and boolean enabled");
|
|
30250
|
+
}
|
|
30251
|
+
const remotePluginId = params["remotePluginId"];
|
|
30252
|
+
if (remotePluginId !== undefined && (typeof remotePluginId !== "string" || !remotePluginId.trim())) {
|
|
30253
|
+
throw RequestError.invalidParams(undefined, "Expected a non-empty remotePluginId");
|
|
30254
|
+
}
|
|
30255
|
+
return { pluginId: params["pluginId"], enabled: params["enabled"], ...typeof remotePluginId === "string" ? { remotePluginId } : {} };
|
|
30256
|
+
}
|
|
30098
30257
|
|
|
30099
30258
|
// src/agent/createAgent.ts
|
|
30100
30259
|
function createAgentApp(options) {
|
|
@@ -30106,7 +30265,7 @@ function createAgentApp(options) {
|
|
|
30106
30265
|
};
|
|
30107
30266
|
return agent({ name: options.info.name }).onConnect((connection) => {
|
|
30108
30267
|
agent2 = new CodexAgent(connection.client, options);
|
|
30109
|
-
}).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_history", { parse: parseSessionHistoryParams }, (ctx) => current().sessionHistory(ctx.params, ctx.signal)).onRequest("_codex/session_history_items", { parse: parseSessionHistoryItemsParams }, (ctx) => current().sessionHistoryItems(ctx.params, ctx.signal)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest("_codex/skills_list", { parse: objectParams() }, (ctx) => current().skillsList(ctx.params)).onRequest("_codex/skills_config_write", { parse: objectParams() }, (ctx) => current().skillsConfigWrite(ctx.params)).onRequest("_codex/plugin_list", { parse: objectParams() }, (ctx) => current().pluginList(ctx.params)).onRequest("_codex/plugin_installed", { parse: objectParams() }, (ctx) => current().pluginInstalled(ctx.params)).onRequest("_codex/plugin_install", { parse: objectParams() }, (ctx) => current().pluginInstall(ctx.params)).onRequest("_codex/plugin_uninstall", { parse: objectParams() }, (ctx) => current().pluginUninstall(ctx.params)).onRequest("_codex/plugin_read", { parse: objectParams() }, (ctx) => current().pluginRead(ctx.params)).onRequest("_codex/marketplace_add", { parse: objectParams() }, (ctx) => current().marketplaceAdd(ctx.params)).onRequest("_codex/marketplace_remove", { parse: objectParams() }, (ctx) => current().marketplaceRemove(ctx.params)).onRequest("_codex/marketplace_upgrade", { parse: objectParams() }, (ctx) => current().marketplaceUpgrade(ctx.params)).onRequest("_codex/session_rename", { parse: parseSessionRenameParams }, (ctx) => current().sessionRename(ctx.params)).onRequest("_codex/account_read", { parse: objectParams() }, () => current().accountRead()).onRequest("_codex/rate_limits", { parse: objectParams() }, () => current().rateLimits()).onRequest("_codex/fuzzy_file_search", { parse: parseFuzzyFileSearchParams }, (ctx) => current().fuzzyFileSearch(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
|
|
30268
|
+
}).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest("_codex/session_list", { parse: parseSessionListParams }, (ctx) => current().sessionList(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_history", { parse: parseSessionHistoryParams }, (ctx) => current().sessionHistory(ctx.params, ctx.signal)).onRequest("_codex/session_history_items", { parse: parseSessionHistoryItemsParams }, (ctx) => current().sessionHistoryItems(ctx.params, ctx.signal)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest("_codex/skills_list", { parse: objectParams() }, (ctx) => current().skillsList(ctx.params)).onRequest("_codex/skills_config_write", { parse: objectParams() }, (ctx) => current().skillsConfigWrite(ctx.params)).onRequest("_codex/plugin_list", { parse: objectParams() }, (ctx) => current().pluginList(ctx.params)).onRequest("_codex/plugin_installed", { parse: objectParams() }, (ctx) => current().pluginInstalled(ctx.params)).onRequest("_codex/plugin_install", { parse: objectParams() }, (ctx) => current().pluginInstall(ctx.params)).onRequest("_codex/plugin_uninstall", { parse: objectParams() }, (ctx) => current().pluginUninstall(ctx.params)).onRequest("_codex/plugin_set_enabled", { parse: parsePluginSetEnabledParams }, (ctx) => current().pluginSetEnabled(ctx.params)).onRequest("_codex/plugin_read", { parse: objectParams() }, (ctx) => current().pluginRead(ctx.params)).onRequest("_codex/marketplace_add", { parse: objectParams() }, (ctx) => current().marketplaceAdd(ctx.params)).onRequest("_codex/marketplace_remove", { parse: objectParams() }, (ctx) => current().marketplaceRemove(ctx.params)).onRequest("_codex/marketplace_upgrade", { parse: objectParams() }, (ctx) => current().marketplaceUpgrade(ctx.params)).onRequest("_codex/session_rename", { parse: parseSessionRenameParams }, (ctx) => current().sessionRename(ctx.params)).onRequest("_codex/account_read", { parse: objectParams() }, () => current().accountRead()).onRequest("_codex/rate_limits", { parse: objectParams() }, () => current().rateLimits()).onRequest("_codex/fuzzy_file_search", { parse: parseFuzzyFileSearchParams }, (ctx) => current().fuzzyFileSearch(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
|
|
30110
30269
|
}
|
|
30111
30270
|
|
|
30112
30271
|
// src/codex/process.ts
|
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
# Wire contract
|
|
2
|
+
|
|
3
|
+
`codex-acp-v2` implements the ACP v2 draft as shipped in `@agentclientprotocol/sdk`
|
|
4
|
+
`experimental/v2`. This page lists what the agent accepts, what it emits, and the
|
|
5
|
+
`_meta.codex` keys it adds. Anything not listed is standard ACP behaviour.
|
|
6
|
+
|
|
7
|
+
## Handshake
|
|
8
|
+
|
|
9
|
+
`initialize` requires `protocolVersion: 2` and `info`. The response advertises:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"capabilities": {
|
|
14
|
+
"session": {
|
|
15
|
+
"prompt": {"image": {}, "embeddedContext": {}},
|
|
16
|
+
"mcp": {"stdio": {}, "http": {}},
|
|
17
|
+
"fork": {}, "delete": {}, "additionalDirectories": {}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"authMethods": [
|
|
21
|
+
{"type": "agent", "methodId": "api-key"},
|
|
22
|
+
{"type": "agent", "methodId": "chat-gpt"},
|
|
23
|
+
{"type": "agent", "methodId": "chat-gpt-device-code"}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`chat-gpt` is omitted when `NO_BROWSER` is set; `chat-gpt-device-code` is offered only
|
|
29
|
+
to clients that declare `capabilities.elicitation.url`. Every other method returns
|
|
30
|
+
`-32600` until `initialize` has succeeded.
|
|
31
|
+
Concurrent initialization requests share one Codex handshake. A failed handshake
|
|
32
|
+
does not unlock session methods and can be retried.
|
|
33
|
+
|
|
34
|
+
`auth/login` with `api-key` reads `_meta["api-key"].apiKey`, then `CODEX_API_KEY`,
|
|
35
|
+
then `OPENAI_API_KEY`. `session/new` returns `-32000` (auth required) while Codex has
|
|
36
|
+
no account.
|
|
37
|
+
|
|
38
|
+
## Providers
|
|
39
|
+
|
|
40
|
+
The agent advertises `capabilities.providers` and exposes one slot, `providerId: "openai"`,
|
|
41
|
+
which is where Codex's OpenAI-protocol traffic goes.
|
|
42
|
+
|
|
43
|
+
| Method | Behaviour |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `providers/list` | Reports the committed routing for new sessions with `supported: ["openai"]` and its `baseUrl`. |
|
|
46
|
+
| `providers/set` | `{providerId: "openai", apiType: "openai", baseUrl, headers?}` routes Codex through that gateway: new and open sessions get a `model_providers.<id>` config entry and `modelProvider: "<id>"`, where `<id>` is `_meta.codex.id` (default `custom-gateway`). Open sessions with persisted history are unsubscribed and resumed with the new routing; a running turn makes the request fail with `-32600`. |
|
|
47
|
+
| `providers/disable` | `{providerId: "openai"}` restores native routing for every gateway; with `_meta.codex.id` only that gateway is removed (catalog mode). Other provider ids are a no-op. |
|
|
48
|
+
|
|
49
|
+
Accepted hints on `providers/set._meta`:
|
|
50
|
+
|
|
51
|
+
| Hint | Meaning |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `codex.id` | Gateway id, `[a-z0-9-]+`, default `custom-gateway`: the `model_providers` entry, the `modelProvider` value and (catalog mode) the select group. Codex's own provider ids (`openai`, the configured `model_provider`) and the native group id `chatgpt` are refused. |
|
|
54
|
+
| `alwith.models` | `[{id, label?, description?}]`: the models the gateway serves; Codex cannot list them. Model ids must be unique across gateways; a clash is refused with `-32602` naming the other gateway. |
|
|
55
|
+
| `alwith.model` | The model to select on the gateway. |
|
|
56
|
+
| `codex.name` | Label of the `model_providers` entry (and of the gateway's select group in catalog mode). |
|
|
57
|
+
| `codex.bearerToken` | Written as `experimental_bearer_token` on the entry, the key DeepSeek's official Codex integration uses; Codex adds the `Authorization` header itself. |
|
|
58
|
+
| `codex.config` | Extra top-level Codex thread-config keys that apply only to threads on the gateway (`model_catalog_json`, `web_search`, `model_reasoning_effort`, …). Must be an object; `model_providers` is refused. When it names a `model_catalog_json` file, the adapter reads it so the gateway's models carry their real reasoning levels, modalities and display names (`model/list` never reflects a per-thread catalog). |
|
|
59
|
+
| `codex.mode` | `"route"` (default): the gateway takes over every session. `"catalog"`: see below. |
|
|
60
|
+
|
|
61
|
+
The gateway must implement the OpenAI Responses API; Codex 0.153 no longer speaks
|
|
62
|
+
chat completions. A session on the gateway does not require an OpenAI login.
|
|
63
|
+
|
|
64
|
+
### Catalog mode
|
|
65
|
+
|
|
66
|
+
`providers/set` with `_meta.codex.mode: "catalog"` registers a gateway without re-routing
|
|
67
|
+
anything. Any number of gateways can be registered this way, each under its own
|
|
68
|
+
`codex.id`; a route-mode `providers/set` replaces them all. `providers/list` keeps
|
|
69
|
+
reporting native routing, with every gateway under `providers[]._meta.codex.gateways`
|
|
70
|
+
(`[{id, name, baseUrl, models}]`; `_meta.codex.gateway` is the first one, kept for older
|
|
71
|
+
clients), and every session's `model` option becomes one group per source: `chatgpt`
|
|
72
|
+
(named ChatGPT: the models Codex serves through the ChatGPT sign-in) followed by one group per gateway (groupId = its id, named after
|
|
73
|
+
`codex.name`, listing its `alwith.models`). Advertised as
|
|
74
|
+
`capabilities._meta.codex.providerCatalog: true`.
|
|
75
|
+
|
|
76
|
+
- `session/set_config_option {configId: "model"}` with a gateway model moves **only that
|
|
77
|
+
session** to the gateway that serves the model (a thread's config carries only that
|
|
78
|
+
gateway's `model_providers` entry); a native model moves it back. Codex has no per-turn provider
|
|
79
|
+
(`turn/start` overrides the model only), so the move is the same unsubscribe + cold
|
|
80
|
+
`thread/resume` an agent-wide change uses, with the gateway's `model_providers` entry and
|
|
81
|
+
`codex.config` overrides in that thread's config. A thread Codex has not materialized yet
|
|
82
|
+
(no user message) is materialized first by injecting a one-line developer note, because
|
|
83
|
+
Codex refuses to cold-resume it otherwise. `effort` then lists the gateway model's levels.
|
|
84
|
+
A running turn on that session → `-32600`.
|
|
85
|
+
- `session/new` and `session/fork` stay native unless `_meta.alwith.model` names a gateway
|
|
86
|
+
model. `session/resume` opens the thread on what `_meta.alwith.model` asks for, else on
|
|
87
|
+
the provider Codex recorded for it (`thread/read`). Codex records the provider a thread
|
|
88
|
+
was **created** with, not one it was moved to later, so a client that moved a thread
|
|
89
|
+
should pass `_meta.alwith.model` when it resumes it.
|
|
90
|
+
- `providers/disable` returns gateway sessions to native routing and drops the gateway
|
|
91
|
+
groups (an empty gateway session is materialized the same way first); with
|
|
92
|
+
`_meta.codex.id` only that gateway's sessions move and only its group goes.
|
|
93
|
+
Re-registering a gateway under the same id (a rotated key) re-resumes the sessions on it
|
|
94
|
+
with the new entry and leaves sessions on other gateways alone.
|
|
95
|
+
- Catalog mode does not affect ALwith Desktop's agent-wide `providers/set` (no `mode` hint).
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Sessions
|
|
100
|
+
|
|
101
|
+
| Method | Notes |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `session/new` | `cwd` must be absolute. `additionalDirectories` become trusted projects and sandbox write roots. `mcpServers` (stdio, http) are added to the thread config; names that collide with the user's Codex config are skipped. |
|
|
104
|
+
| `session/resume` | `replayFrom: {type: "start"}` replays the transcript as `session/update` frames before the response, paged through Codex `thread/turns/list` in pages of 50 turns; `null` or omitted restores context only. Other cursors are rejected. When Codex answers "already has an active writer" (its writer lock is a file lock across the Codex home: the thread is open in another Codex client — ChatGPT app, CLI, another app-server — and nothing here can release it) the session opens for viewing instead, the way Codex's TUI does: `thread/read` in place of `thread/resume`, history through the same `thread/turns/list` path, the response carries `_meta: {codex: {readOnly: true, reason: "active_writer"}}`. On such a session `session/prompt` and `session/set_config_option` fail with `-32600` "Thread is open in another Codex client" and `data: {codex: {readOnly: true}}`; `session/cancel` is a notification and is ignored. Resuming the same id again retries the real resume and, when the other client has let go, replaces the viewing session. |
|
|
105
|
+
| `session/fork` | Forks the Codex thread and replays the copied transcript under the new session id. The source session remains open, including any running turn. |
|
|
106
|
+
| `session/list` | `cwd` filters by exact Codex thread cwd; `cursor` pages. |
|
|
107
|
+
| `_codex/session_list` | Without params, returns all non-archived sessions across projects in one response. With params, delegates to `session/list`. Advertised as `capabilities._meta.codex.sessionList: {version: 1}`. |
|
|
108
|
+
| `session/close` | Detaches locally within a single 5-second budget, subject to event-loop scheduling. At most half is spent waiting for an interrupted turn; the remainder is reserved for unsubscribe. Stalled client writes do not prevent cleanup. A late start is interrupted when its id becomes known. |
|
|
109
|
+
| `session/delete` | Close plus `thread/delete` (permanent deletion). |
|
|
110
|
+
| `_codex/session_history` | Read-only turn metadata or full ACP history without opening or subscribing to the session. See the contract below. Advertised as `capabilities._meta.codex.sessionHistory: {version: 2, items: true, modes: ["browse", "export"]}`. |
|
|
111
|
+
| `_codex/session_history_items` | Independent item pagination across a thread or within one turn; supports item anchors and optional native data. Part of the version 2 history capability. |
|
|
112
|
+
| `_codex/session_archive` | `{sessionId}` closes and archives the thread (reversible hiding). Advertised as `capabilities._meta.codex.archive: true`. |
|
|
113
|
+
| `_codex/session_unarchive` | `{sessionId}` restores an archived thread's visibility. |
|
|
114
|
+
| `_codex/session_rename` | `{sessionId, name}` → `thread/name/set`. A loaded thread then reports `thread/name/updated`, delivered as `session_info_update` with the new `title`; a thread that is not loaded gets no notification. Advertised as `capabilities._meta.codex.rename: true`. |
|
|
115
|
+
| `session/set_config_option` | Returns and broadcasts the full option list. |
|
|
116
|
+
|
|
117
|
+
`session/list` with `_meta: {codex: {archived: true}}` lists archived threads.
|
|
118
|
+
Both lists include all model providers, including custom gateways. `cwd` must match
|
|
119
|
+
the stored canonical path exactly (use the path returned by Codex, especially on
|
|
120
|
+
platforms where temporary directories have symlink aliases).
|
|
121
|
+
The default is the non-archived list; each returned session includes
|
|
122
|
+
`_meta.codex.archived`.
|
|
123
|
+
|
|
124
|
+
`_codex/session_list` accepts the same request fields and returns the same session
|
|
125
|
+
entries as `session/list`. Omitted params, `null`, `{}`, or an object containing
|
|
126
|
+
only `undefined` values select the full-list mode. Any defined property (including
|
|
127
|
+
`cwd`, `cursor`, or `_meta`, even when null) selects normal single-page behavior.
|
|
128
|
+
For example, `{_meta: {codex: {archived: true}}}` returns one archive page;
|
|
129
|
+
`{cwd: "/project"}` returns one project page. Standard `session/list` is unchanged.
|
|
130
|
+
|
|
131
|
+
Full-list mode walks native pages of 100 sessions, deduplicates by session ID,
|
|
132
|
+
then supplements native forks once. Title repair reads are limited to 8 at a time.
|
|
133
|
+
It returns `{sessions}` without `nextCursor`, only after every page succeeds.
|
|
134
|
+
A failed page or repeated cursor rejects the request instead of returning a partial
|
|
135
|
+
list. It uses the same source-kind and model-provider filters as `session/list`;
|
|
136
|
+
"all" does not include archived threads or excluded internal subagent sources.
|
|
137
|
+
There is no persistent cache or atomic snapshot guarantee across native pages.
|
|
138
|
+
|
|
139
|
+
`session/new` accepts `_meta.codex.seedHistory: [{role, text}]`, with `role` equal
|
|
140
|
+
to `"user"` or `"assistant"`. These entries are injected into the new thread as
|
|
141
|
+
model-visible history before the first prompt. This extension is advertised as
|
|
142
|
+
`capabilities._meta.codex.seedHistory: true`.
|
|
143
|
+
|
|
144
|
+
Invalid replay cursors, additional directories, and seed history are rejected
|
|
145
|
+
before an existing session is closed or a new thread is created.
|
|
146
|
+
|
|
147
|
+
### Read-only history (version 2)
|
|
148
|
+
|
|
149
|
+
`capabilities._meta.codex.sessionHistory` advertises
|
|
150
|
+
`{version: 2, items: true, modes: ["browse", "export"]}`. Both history methods require
|
|
151
|
+
`initialize`, but not `session/resume`. They do not acquire a writer, create a runtime,
|
|
152
|
+
change subscriptions, interrupt a turn, unarchive a thread, or publish `session/update`
|
|
153
|
+
notifications. They read Codex's persisted history through its app-server, including
|
|
154
|
+
closed, archived, and other clients' threads. No second transcript store is created.
|
|
155
|
+
|
|
156
|
+
#### Turn pages and item pages
|
|
157
|
+
|
|
158
|
+
Get the latest turn summaries for a history browser:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{"sessionId": "thread-id", "sortDirection": "desc", "limit": 20}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Send that request to `_codex/session_history`. `itemsView` defaults to `"summary"`:
|
|
165
|
+
only turn metadata is returned, with empty `updates` and `omissions`. Choose `"full"`
|
|
166
|
+
to project whole turns, or use `_codex/session_history_items` for bounded item pages:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{"sessionId": "thread-id", "turnId": "turn-id", "limit": 20, "includeNative": true}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The item method accepts `turnId` to restrict the result to one turn. Omit it to page
|
|
173
|
+
items across the thread. `anchorItemId` requires `turnId` and starts exclusively after
|
|
174
|
+
that item in ascending order, or before it in descending order; it cannot be combined
|
|
175
|
+
with `cursor`. Continue from the returned cursor without repeating the anchor.
|
|
176
|
+
Turn IDs are obtained from the turn method; no full-turn load is required to locate
|
|
177
|
+
and read a turn's items.
|
|
178
|
+
|
|
179
|
+
Common parameters:
|
|
180
|
+
|
|
181
|
+
| Parameter | Contract |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `sessionId` | Required non-empty Codex thread ID. |
|
|
184
|
+
| `cursor` | Opaque next cursor, null/omitted for the first page. Bound to the session, endpoint, direction, mode, view, native-data choice and (for items) turn filter. Preserve those options when continuing. |
|
|
185
|
+
| `limit` | Integer 1–100, default 50. Counts turns or items according to the endpoint. Can change between pages. |
|
|
186
|
+
| `sortDirection` | `"asc"` (default) or `"desc"`. Order within a whole turn remains chronological; the item endpoint orders individual items in the chosen direction. Start a new traversal to change direction. |
|
|
187
|
+
| `mode` | `"browse"` (default) or `"export"`, described below. |
|
|
188
|
+
| `includeNative` | Default false. Retain exact native item records under `_meta.codex`. On the turn method this requires `itemsView: "full"`. |
|
|
189
|
+
| `maxBytes` | Maximum serialized **result payload** size in bytes, excluding the JSON-RPC envelope. Default 1 MiB, allowed range 1 KiB–16 MiB. Can change between pages. |
|
|
190
|
+
|
|
191
|
+
Unknown parameters, invalid types, incompatible options and version 1 cursors fail
|
|
192
|
+
explicitly. Version 2 changes the unreleased version 1 default from full export to
|
|
193
|
+
summary browsing and replaces `omittedItems` with `omissions`.
|
|
194
|
+
|
|
195
|
+
Both responses contain `sessionId`, `cwd`, `title` (explicit name or preview, nullable),
|
|
196
|
+
`createdAt`, `updatedAt` (Unix **seconds**), `forkedFromId`, `running`, `consistency`,
|
|
197
|
+
`revision`, `nextCursor`, and `complete`. `running` is the observed thread/local prompt
|
|
198
|
+
state, not a subscription or a guarantee about the state after the response. It is
|
|
199
|
+
null when Codex reports an unloaded/unknown state: absence of a local runtime does
|
|
200
|
+
not prove that another client is idle.
|
|
201
|
+
|
|
202
|
+
- Turn responses add `turns[]`: `turnId`, `status`, `error`, `startedAt`, `completedAt`
|
|
203
|
+
(nullable Unix **seconds**), `durationMs`, `itemsView`, `updates`, `omissions`, and
|
|
204
|
+
optional `_meta.codex.items` containing full native `ThreadItem` records.
|
|
205
|
+
- Item responses add `items[]`: `turnId`, `itemId` (native item ID), `startedAtMs`,
|
|
206
|
+
`completedAtMs` (nullable Unix **milliseconds**), `updates`, `omissions`, and optional
|
|
207
|
+
`_meta.codex.item`. Item timestamps are not misreported as turn timestamps.
|
|
208
|
+
- `updates` contains standard ACP update payloads as response data, never notifications.
|
|
209
|
+
Apply tool upserts in order. Message IDs preserve the existing receipt-ID convention;
|
|
210
|
+
retained `/plan <task>` and `/review` commands use their original ACP receipt and
|
|
211
|
+
content, while internal plan implementation prompts have no ACP message update.
|
|
212
|
+
`_meta.codex.turnId` and the enclosing turn/item identify provenance. Full-turn message
|
|
213
|
+
updates also carry `turnStartedAt` in milliseconds when known.
|
|
214
|
+
- `complete` means exactly `nextCursor === null`, the end of **this traversal**. It is
|
|
215
|
+
not a complete-content, immutable-snapshot, or full-thread assertion: a traversal
|
|
216
|
+
may be anchored, turn-filtered, summary-only, or missing previously collected pages.
|
|
217
|
+
|
|
218
|
+
#### Browsing versus export
|
|
219
|
+
|
|
220
|
+
`mode: "browse"` allows running sessions and returns `consistency: "live"`,
|
|
221
|
+
`revision: null`. The cursor remains usable when a new turn is appended. Codex may
|
|
222
|
+
change a returned in-progress item, or reject an invalidated cursor after rollback;
|
|
223
|
+
clients should upsert by turn/item identity and refresh or restart as appropriate.
|
|
224
|
+
This is suitable for history viewers, loading older messages, inspection and monitoring.
|
|
225
|
+
Each page requires one metadata read and one turn/item page read.
|
|
226
|
+
|
|
227
|
+
`mode: "export"` returns `consistency: "optimistic"` and a change fingerprint in
|
|
228
|
+
`revision`. It rejects a known running session, including a locally accepted prompt
|
|
229
|
+
not yet persisted by Codex. Before and after each page it compares thread metadata
|
|
230
|
+
and the last turn's **summary**, with the fingerprint carried by continuation cursors.
|
|
231
|
+
It no longer reloads a large final tool result merely to check every page boundary.
|
|
232
|
+
A detected change invalidates the traversal: discard collected pages and restart.
|
|
233
|
+
Use the same mode throughout a traversal and wait for the thread to become quiescent.
|
|
234
|
+
|
|
235
|
+
Example for textual/ACP export:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{"sessionId": "thread-id", "mode": "export", "itemsView": "full", "limit": 10}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Export mode is a best-effort conflict check, **not an atomic snapshot or a content
|
|
242
|
+
hash of the entire history**. Codex has no transactional history version and its
|
|
243
|
+
thread timestamps have second precision. Changes to earlier items, or details absent
|
|
244
|
+
from a summary, can evade detection with unchanged timestamps/summary. No guarantee
|
|
245
|
+
extends past the final check. Strict backup/audit snapshots require stronger support
|
|
246
|
+
from Codex itself; version 2 does not claim to provide them.
|
|
247
|
+
|
|
248
|
+
#### Content fidelity and size
|
|
249
|
+
|
|
250
|
+
The new endpoints use a history-specific projection without changing live replay.
|
|
251
|
+
Mixed text, audio, images, skills and mentions retain separate content blocks. URL
|
|
252
|
+
and local media become `resource_link` blocks with `_meta.codex.inputType`; local paths
|
|
253
|
+
become file URIs. File-ID-only images use `codex-file:<encoded-id>` and retain `fileId`.
|
|
254
|
+
These are references, not fetched media or an automatic resource resolver. Text spans
|
|
255
|
+
are retained under `_meta.codex.textElements`.
|
|
256
|
+
|
|
257
|
+
`omissions[]` records `{itemId, field, reason}` for known projection losses, including
|
|
258
|
+
unmapped items (`field: "*"`), reasoning content replaced by a summary, and unrepresented
|
|
259
|
+
assistant fields. It is a diagnostic list, not a proof that every native field was
|
|
260
|
+
represented. For lossless capture of **native item records returned by Codex**, request
|
|
261
|
+
`includeNative: true`; the raw records remain namespaced under `_meta.codex` and follow
|
|
262
|
+
Codex's versioned schema. These records cannot recover data Codex never persisted,
|
|
263
|
+
truncated tool output, missing host metadata, or unavailable external files.
|
|
264
|
+
|
|
265
|
+
Responses exceeding `maxBytes` fail with `history_page_too_large` and `bytes`/`maxBytes`
|
|
266
|
+
diagnostics. Nothing is truncated and no continuation is consumed: retry the same
|
|
267
|
+
cursor with a smaller limit or a larger budget. To switch from whole turns to item
|
|
268
|
+
pagination, start the item endpoint without reusing a turn cursor. A single item can
|
|
269
|
+
still exceed the maximum; this fails explicitly rather than silently omitting it.
|
|
270
|
+
The cap applies after the native reply has been materialized; it bounds outgoing
|
|
271
|
+
payloads, not Codex's allocations or peak adapter memory. There is no chunked blob
|
|
272
|
+
transfer in this protocol.
|
|
273
|
+
|
|
274
|
+
#### Errors and cancellation
|
|
275
|
+
|
|
276
|
+
History errors include `data.reason` and `data.retryable`. Native failures also retain
|
|
277
|
+
`data.native.code` and `data.native.data`; numeric native RPC codes are preserved.
|
|
278
|
+
`retryable: null` means unknown, not an invitation to retry automatically. Known native
|
|
279
|
+
not-found/cursor failures are classified; unknown native errors remain intact.
|
|
280
|
+
|
|
281
|
+
| Reason | Caller action |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| `history_invalid_params` / `history_invalid_cursor` | Correct the request or start a new traversal; false. |
|
|
284
|
+
| `history_busy` | Wait for idle, then retry export; true. |
|
|
285
|
+
| `history_changed` | Discard all collected pages and restart without a cursor; true. |
|
|
286
|
+
| `history_not_found` | Report the missing source; false. |
|
|
287
|
+
| `history_unsupported` | Upgrade the Codex app-server; false. |
|
|
288
|
+
| `history_unavailable` | Retry after the reported source/transport problem is resolved; true. |
|
|
289
|
+
| `history_source_error` | Inspect preserved native diagnostics; retryability unknown. |
|
|
290
|
+
| `history_incomplete` / `history_invalid_page` | Do not publish an export; investigate the native response; false. |
|
|
291
|
+
| `history_page_too_large` | Retry with a smaller page or larger budget; true. |
|
|
292
|
+
| `history_cancelled` | Stop this traversal; false. |
|
|
293
|
+
| `command_history_incomplete` | A command receipt mapping is unconfirmed in this thread or an ancestor; wait for saving to finish, or restore the command history data; retryability unknown. |
|
|
294
|
+
|
|
295
|
+
Invalid inputs use `-32602`; local state conflicts/size/cancellation use `-32600`;
|
|
296
|
+
incomplete/non-progressing native pages use `-32603`. No error becomes an empty
|
|
297
|
+
successful archive. Clients should track continuation cursors to detect longer cycles.
|
|
298
|
+
|
|
299
|
+
ACP `$/cancel_request` cancels an individual history request. It stops waiting and
|
|
300
|
+
prevents further native reads, including while saved command receipts are being
|
|
301
|
+
loaded. An already started native or receipt read may finish in the background;
|
|
302
|
+
the cancelled request returns `history_cancelled`. It never sends `turn/interrupt`.
|
|
303
|
+
These methods do not upload exports or
|
|
304
|
+
perform the separate reversible `_codex/session_archive` operation.
|
|
305
|
+
|
|
306
|
+
### Config options
|
|
307
|
+
|
|
308
|
+
| `configId` | `category` | Type | Values |
|
|
309
|
+
| --- | --- | --- | --- |
|
|
310
|
+
| `mode` | `mode` | select | `read-only`, `agent`, `agent-full-access` |
|
|
311
|
+
| `model` | `model` | select | Codex catalog ids (the current id is always listed) |
|
|
312
|
+
| `effort` | `thought_level` | select | efforts supported by the current model |
|
|
313
|
+
| `collaboration_mode` | `model_config` | select | `default`, `plan` |
|
|
314
|
+
| `fast_mode` | `model_config` | boolean | only when the model offers the `fast` service tier |
|
|
315
|
+
|
|
316
|
+
Changing `model` re-validates `effort` and clears `fast_mode` if the new model does
|
|
317
|
+
not support it.
|
|
318
|
+
|
|
319
|
+
## Skills and plugins
|
|
320
|
+
|
|
321
|
+
Codex's skill and plugin catalogs are exposed as `_codex/*` requests whose params and
|
|
322
|
+
results are the Codex app-server v2 shapes, passed through verbatim (see
|
|
323
|
+
`src/app-server/v2/`). Codex validates the fields; the adapter only checks that params
|
|
324
|
+
are an object and requires a successful `initialize`. Codex errors propagate unchanged.
|
|
325
|
+
Advertised as `capabilities._meta.codex.skills: true` and `capabilities._meta.codex.plugins: true`.
|
|
326
|
+
|
|
327
|
+
| Method | Codex request | Params → result |
|
|
328
|
+
| --- | --- | --- |
|
|
329
|
+
| `_codex/skills_list` | `skills/list` | `SkillsListParams` → `SkillsListResponse` |
|
|
330
|
+
| `_codex/skills_config_write` | `skills/config/write` | `SkillsConfigWriteParams` → `SkillsConfigWriteResponse` |
|
|
331
|
+
| `_codex/plugin_list` | `plugin/list` | `PluginListParams` → `PluginListResponse` |
|
|
332
|
+
| `_codex/plugin_installed` | `plugin/installed` | `PluginInstalledParams` → `PluginInstalledResponse` |
|
|
333
|
+
| `_codex/plugin_install` | `plugin/install` | `PluginInstallParams` → `PluginInstallResponse` |
|
|
334
|
+
| `_codex/plugin_uninstall` | `plugin/uninstall` | `PluginUninstallParams` → `{}` |
|
|
335
|
+
| `_codex/plugin_set_enabled` | Local: `config/value/write`; remote: plugin-service and `plugin/reconcile` | `{pluginId: string, enabled: boolean, remotePluginId?: string}` → `{enabled: boolean}` |
|
|
336
|
+
| `_codex/plugin_read` | `plugin/read` | `PluginReadParams` → `PluginReadResponse` |
|
|
337
|
+
| `_codex/marketplace_add` | `marketplace/add` | `MarketplaceAddParams` → `MarketplaceAddResponse` |
|
|
338
|
+
| `_codex/marketplace_remove` | `marketplace/remove` | `MarketplaceRemoveParams` → `{}` |
|
|
339
|
+
| `_codex/marketplace_upgrade` | `marketplace/upgrade` | `MarketplaceUpgradeParams` → `MarketplaceUpgradeResponse` |
|
|
340
|
+
|
|
341
|
+
The agent sends the `_codex/skills_changed` notification (params `{}`) whenever Codex
|
|
342
|
+
emits `skills/changed`, and after each successful `_codex/skills_config_write`,
|
|
343
|
+
`_codex/plugin_install`, `_codex/plugin_uninstall`, `_codex/plugin_set_enabled` and `_codex/marketplace_*` request.
|
|
344
|
+
Plugins ship skills, so one signal covers both catalogs; a host refetches on it.
|
|
345
|
+
The adapter's own skill snapshot and `available_commands_update` handling is unchanged.
|
|
346
|
+
|
|
347
|
+
## Account and file search
|
|
348
|
+
|
|
349
|
+
Pass-throughs in the same style as the skills surface (params and results are the Codex
|
|
350
|
+
shapes, `initialize` required, Codex errors propagate unchanged). Advertised as
|
|
351
|
+
`capabilities._meta.codex.account: true` and `capabilities._meta.codex.fuzzyFileSearch: true`.
|
|
352
|
+
|
|
353
|
+
| Method | Codex request | Params → result |
|
|
354
|
+
| --- | --- | --- |
|
|
355
|
+
| `_codex/account_read` | `account/read {refreshToken: false}` | `{}` → `GetAccountResponse` |
|
|
356
|
+
| `_codex/rate_limits` | `account/rateLimits/read` | `{}` → `GetAccountRateLimitsResponse` |
|
|
357
|
+
| `_codex/fuzzy_file_search` | `fuzzyFileSearch` | `{query, roots, cancellationToken?}` → `FuzzyFileSearchResponse` (`cancellationToken` defaults to `null`) |
|
|
358
|
+
|
|
359
|
+
Notifications the agent forwards with Codex's payload verbatim: `account/rateLimits/updated`
|
|
360
|
+
→ `_codex/rate_limits_updated`, `fuzzyFileSearch/sessionUpdated` →
|
|
361
|
+
`_codex/fuzzy_file_search_updated`, `fuzzyFileSearch/sessionCompleted` →
|
|
362
|
+
`_codex/fuzzy_file_search_completed`. (Inside a session, fuzzy search frames are also
|
|
363
|
+
mapped to `tool_call_update`s as before.)
|
|
364
|
+
|
|
365
|
+
## Prompts and state
|
|
366
|
+
|
|
367
|
+
`session/prompt` inserts a new foreground prompt into the adapter's live conversation
|
|
368
|
+
and starts publishing its `user_message` before returning `{messageId}`, without
|
|
369
|
+
waiting for Codex startup (or returns `-32602` for an empty prompt,
|
|
370
|
+
an image on a text-only model, or an unknown session). The id is minted by the adapter
|
|
371
|
+
unless the request names it in `_meta.alwith.messageId` (a non-empty string a host
|
|
372
|
+
chooses so it can recognise the turn's frames before the response arrives), and it is
|
|
373
|
+
passed to Codex as `clientUserMessageId`, so the `user_message` echo and every
|
|
374
|
+
replay of that message report under it (`userMessage.clientId`; a message written by
|
|
375
|
+
another Codex client has no `clientId` and keeps its item id). While a turn runs, another
|
|
376
|
+
prompt on the same session is injected into it with `turn/steer` and returns
|
|
377
|
+
`{messageId, _meta: {codex: {steered: "<turnId>"}}}`.
|
|
378
|
+
|
|
379
|
+
Turn correlation (`capabilities._meta.alwith.turns = {version: 2}`): every `state_update`
|
|
380
|
+
of a prompted turn (`running`, `requires_action`, `idle`) carries `_meta.alwith.messageId`,
|
|
381
|
+
the receipt of the prompt that owns the turn. A turn Codex started elsewhere (observed
|
|
382
|
+
foreign turn) has no receipt and no such field. An `idle` that belongs to an older turn is
|
|
383
|
+
never published after a newer prompt started its turn on the session.
|
|
384
|
+
Image capability validation also applies to steering. Once `idle` is published,
|
|
385
|
+
the next prompt starts a new turn rather than steering the completed one.
|
|
386
|
+
|
|
387
|
+
State frames:
|
|
388
|
+
|
|
389
|
+
| `state` | When |
|
|
390
|
+
| --- | --- |
|
|
391
|
+
| `running` | Turn accepted. Also after every blocking client request resolves. |
|
|
392
|
+
| `requires_action` | The first open `session/request_permission` or `elicitation/create`. |
|
|
393
|
+
| `idle` | Turn over. `stopReason` is `end_turn` or `cancelled`; `usage` carries the last turn's tokens when known. |
|
|
394
|
+
|
|
395
|
+
A failed turn emits an `agent_message_chunk` with the error text and
|
|
396
|
+
`_meta.codex.error {message, codexErrorInfo, additionalDetails}`, then
|
|
397
|
+
`idle` with `stopReason: "error"` and a standard JSON-RPC `error {code, message, data}`.
|
|
398
|
+
`data.codex` retains category and retryability; `_meta.codex.error` retains Codex diagnostics.
|
|
399
|
+
|
|
400
|
+
`session/cancel` calls `turn/interrupt`; the turn ends with `idle` / `cancelled`.
|
|
401
|
+
Concurrent cancel requests coalesce; native interrupt RPCs coalesce only while in flight within the recovery budget.
|
|
402
|
+
A timed-out RPC does not block a later retry.
|
|
403
|
+
If completion is missing after three seconds, the adapter checks `thread/read` with a
|
|
404
|
+
three-second deadline. Only native `idle` or `notLoaded` confirms interruption and
|
|
405
|
+
allows the adapter to reconcile open tools and publish `idle` / `cancelled`. An
|
|
406
|
+
unloaded thread requires reopening before the next prompt. An active or unreachable
|
|
407
|
+
thread gets one interrupt retry and another bounded status check. If cancellation
|
|
408
|
+
still cannot be confirmed, an actionable agent message is emitted while the session remains
|
|
409
|
+
running; another Stop retries. Recovery is scoped to the original turn and cannot
|
|
410
|
+
cancel or finalize a newer turn or another session.
|
|
411
|
+
Restoring an active turn bounds each command-receipt read to three seconds. Stop
|
|
412
|
+
cancels the restore wait; late reads cannot publish old messages into a later turn.
|
|
413
|
+
Unknown user-message mappings are not replaced with native IDs. A restore failure
|
|
414
|
+
does not mark a still-running native turn idle: updates continue, and normal native
|
|
415
|
+
completion reports the history error; confirmed cancellation still reports
|
|
416
|
+
`idle` / `cancelled`.
|
|
417
|
+
When `replayFrom` is omitted or `null`, restoring the active turn only rebuilds internal
|
|
418
|
+
state; it emits no stored message or tool snapshots, before or after the resume response.
|
|
419
|
+
New live tool activity announces any required initial tool/terminal state before its
|
|
420
|
+
updates. Tools never announced to the client are not manufactured merely to finalize
|
|
421
|
+
them. Explicit `replayFrom: {type: "start"}` still replays retained history.
|
|
422
|
+
Cancellation before `turn/start` prevents that turn from being sent, even if
|
|
423
|
+
the preceding skills refresh is still pending. For `/compact`, cancellation
|
|
424
|
+
ends the adapter's wait and reports `idle` / `cancelled`; Codex exposes no
|
|
425
|
+
compaction-specific interrupt, so background compaction may still finish.
|
|
426
|
+
|
|
427
|
+
Blocking requests are counted per turn: a late response from a previous turn
|
|
428
|
+
cannot clear the current turn's `requires_action` state. A lost Codex connection
|
|
429
|
+
also ends waits for compaction and plan approval with `error`.
|
|
430
|
+
|
|
431
|
+
## Session updates
|
|
432
|
+
|
|
433
|
+
### Chat branches
|
|
434
|
+
|
|
435
|
+
`capabilities._meta.codex.forkAtTurn: true` advertises an optional
|
|
436
|
+
`session/fork._meta.codex.lastTurnId` non-empty string. It maps to Codex
|
|
437
|
+
`thread/fork.lastTurnId`: the referenced completed turn is included and later
|
|
438
|
+
turns are omitted. Codex rejects unknown or in-progress boundaries. Omitting
|
|
439
|
+
the hint preserves the whole-thread fork. Message/item ids are not turn ids.
|
|
440
|
+
|
|
441
|
+
Live `user_message` / `agent_message_chunk` and replayed `user_message` /
|
|
442
|
+
`agent_message` updates carry `_meta.codex.turnId` for their enclosing Codex
|
|
443
|
+
turn, alongside existing phase metadata.
|
|
444
|
+
When Codex supplies the turn's original `startedAt`, those messages also carry
|
|
445
|
+
`_meta.codex.turnStartedAt` as Unix milliseconds, on live delivery and history
|
|
446
|
+
replay alike. It is omitted when native timing is unavailable; clients must not
|
|
447
|
+
display a replay arrival time as the historical turn time.
|
|
448
|
+
|
|
449
|
+
`capabilities._meta.codex.sessionLineage: true` advertises
|
|
450
|
+
`_meta.codex.{nativeSessionId, forkedFromId}` on `session/new`, `session/resume`,
|
|
451
|
+
`session/fork` responses and each `session/list` entry. These are Codex's native
|
|
452
|
+
`Thread.sessionId` and `Thread.forkedFromId`. ACP `sessionId` still identifies
|
|
453
|
+
one thread. Despite its upstream type comment, native `Thread.sessionId` can
|
|
454
|
+
change on fork (verified with Codex 0.156.1); clients must build branch relations
|
|
455
|
+
from `forkedFromId`, not equality of `nativeSessionId`. A root has
|
|
456
|
+
`forkedFromId: null`. Subagent `parentThreadId` is unrelated. Clients must page
|
|
457
|
+
the list fully when looking for branches; a branch can have a different cwd.
|
|
458
|
+
The adapter does not persist a separate branch index.
|
|
459
|
+
|
|
460
|
+
| Update | Source |
|
|
461
|
+
| --- | --- |
|
|
462
|
+
| `agent_message_chunk` | `item/agentMessage/delta`; `messageId` is the Codex item id; `_meta.codex.phase` is `commentary` or `final_answer`. Notices (warnings, model reroutes) use `_meta.codex.notice: true`. |
|
|
463
|
+
| `agent_thought_chunk` | reasoning deltas, keyed by item id |
|
|
464
|
+
| `user_message` | local prompt insertion, native `userMessage` upserts, and retained history replay; the ACP receipt ID identifies prompts inserted through `session/prompt` |
|
|
465
|
+
| `agent_message`, `agent_thought` | history replay; `agent_message` also reports cancellation recovery and command-save problems requiring user action |
|
|
466
|
+
| `tool_call_update` | see below |
|
|
467
|
+
| `terminal_update`, `terminal_output_chunk` | shell commands; `terminalId` equals the tool call id; data is base64 |
|
|
468
|
+
| `plan_update` | `turn/plan/updated` → `{type: "items", planId: "codex-turn-plan"}`; plan-mode drafts → `{type: "markdown", planId: <item id>}` |
|
|
469
|
+
| `usage_update` | `thread/tokenUsage/updated`: `used` = last turn total tokens, `size` = model context window |
|
|
470
|
+
| `compaction_update` | `compactionId` = Codex `contextCompaction` item id; `in_progress` on item/started, `completed` on item/completed (history snapshots report `completed`). No `compaction_summary_chunk`: Codex exposes no summary text. |
|
|
471
|
+
| `session_info_update` | `title` from Codex thread names, or the first prompt line as a fallback; `_meta.codex.retry` for transient errors Codex is retrying |
|
|
472
|
+
| `available_commands_update` | built-in slash commands plus `$<skill>` entries |
|
|
473
|
+
| `config_option_update` | after every `session/set_config_option` and `/plan` |
|
|
474
|
+
|
|
475
|
+
### Tool calls
|
|
476
|
+
|
|
477
|
+
The first frame for a `toolCallId` carries `name`, `title`, and `kind`; later frames
|
|
478
|
+
omit `name` and patch `status`, `content`, `rawOutput`. Codex `interrupted` items map
|
|
479
|
+
to status `cancelled`, and a cancelled turn marks every still-open tool call
|
|
480
|
+
`cancelled` before the `idle` frame.
|
|
481
|
+
|
|
482
|
+
| `name` | `kind` | Codex item |
|
|
483
|
+
| --- | --- | --- |
|
|
484
|
+
| `shell` | `execute` | unclassified command; `content: [{type: "terminal"}]` |
|
|
485
|
+
| `read_file`, `list_files` | `read` | command classified as a read or listing |
|
|
486
|
+
| `search` | `search` | command classified as a search |
|
|
487
|
+
| `apply_patch` | `edit` | file change; `content` is `diff` with `changes` and `patch {format: "git_patch"}`, `locations` lists every path |
|
|
488
|
+
| `mcp` | `execute` | MCP tool call; progress arrives as text content |
|
|
489
|
+
| `dynamic_tool` | `execute` | dynamic tool call |
|
|
490
|
+
| `web_search` | `fetch` | web search |
|
|
491
|
+
| `view_image` | `read` | image view |
|
|
492
|
+
| `image_generation` | `other` | image generation; result as image content |
|
|
493
|
+
| `subagent`, `collab` | `other` | sub-agent activity and collaboration calls; `_meta.codex.subagent` / `_meta.codex.collaboration` |
|
|
494
|
+
| `fuzzy_file_search` | `search` | Codex fuzzy file search sessions |
|
|
495
|
+
| `guardian_review` | `think` | auto-approval reviews |
|
|
496
|
+
| `mcp_startup` | `other` | failed MCP server startups (status `failed`) |
|
|
497
|
+
| `plan_review` | `switch_mode` | plan approval prompt |
|
|
498
|
+
|
|
499
|
+
## Permissions
|
|
500
|
+
|
|
501
|
+
All approvals use `session/request_permission` with `title`, optional `description`
|
|
502
|
+
(the Codex reason), and `subject: {type: "tool_call", toolCall}` whose `toolCallId`
|
|
503
|
+
is the Codex item id. Clients answer with an advertised `optionId`; `cancelled`,
|
|
504
|
+
unknown ids, and transport errors fail closed.
|
|
505
|
+
|
|
506
|
+
| Title | Options (`optionId` → Codex decision) |
|
|
507
|
+
| --- | --- |
|
|
508
|
+
| `Run command?` / `Allow network access?` | `allow_once` → accept, `allow_for_session` → acceptForSession, `accept_execpolicy_amendment` → acceptWithExecpolicyAmendment, `apply_network_policy_amendment:<n>` → applyNetworkPolicyAmendment, `decline`, `cancel`. When Codex sends `availableDecisions` that list is authoritative. |
|
|
509
|
+
| `Make edits?` | `allow_once` → accept, `allow_for_session` → acceptForSession, `cancel` |
|
|
510
|
+
| `Grant permissions?` | `allow_permissions_turn`, `allow_permissions_turn_strict_auto_review`, `allow_permissions_session`, `reject_permissions` |
|
|
511
|
+
| MCP approvals | `allow_once` / `accept`, `allow_session`, `allow_always` (only when Codex advertises `persist`), `decline`, `cancel` |
|
|
512
|
+
| `Implement this plan?` | `implement_plan`, `revise_plan` |
|
|
513
|
+
|
|
514
|
+
Option descriptions ride in `_meta.codex.description`.
|
|
515
|
+
|
|
516
|
+
## Elicitation
|
|
517
|
+
|
|
518
|
+
- MCP form elicitations use `elicitation/create` (`mode: "form"`) when the client
|
|
519
|
+
declares `elicitation.form`; MCP `enum`/`enumNames` schemas are converted to `oneOf`.
|
|
520
|
+
A structured form the client cannot render is cancelled rather than degraded.
|
|
521
|
+
- MCP URL elicitations use `mode: "url"` when the client declares `elicitation.url`;
|
|
522
|
+
`elicitation/complete` follows once Codex resolves the request.
|
|
523
|
+
- Message-only MCP requests fall back to `session/request_permission`.
|
|
524
|
+
- Codex user-input questions (`item/tool/requestUserInput`) become a form whose
|
|
525
|
+
`toolCallId` is the Codex item; questions with an "other" answer add a
|
|
526
|
+
`<id>__other` text field.
|
|
527
|
+
|
|
528
|
+
### Shutdown and routing failure details
|
|
529
|
+
|
|
530
|
+
`session/close` reports `_meta.codex.close` with `attempted: true`,
|
|
531
|
+
`localDetached: true`, and `remoteUnsubscribe: "confirmed" | "timed_out" | "failed"`
|
|
532
|
+
when a loaded session is closed. Closing an unknown session remains a no-op.
|
|
533
|
+
A timeout confirms only local cleanup, not remote unsubscription. Terminal updates
|
|
534
|
+
are best effort: a completed local write is not acknowledgement by the client.
|
|
535
|
+
Codex stdout EOF drains already received RPC frames through the dispatcher before
|
|
536
|
+
publishing connection loss; an already dispatched turn completion remains authoritative.
|
|
537
|
+
Shutdown does not wait for user approvals to finish.
|
|
538
|
+
|
|
539
|
+
Provider changes are staged and committed only after all open sessions rebind.
|
|
540
|
+
All sessions' history is checked before changing subscriptions. Codex cannot resume
|
|
541
|
+
an empty thread whose history storage has not yet been created; in that case the
|
|
542
|
+
request fails with `-32600` before changing routing. Close empty sessions, configure
|
|
543
|
+
the provider, then create them again. Rebinding unsubscribes before resume because
|
|
544
|
+
Codex treats resume of a subscribed thread as rejoin and ignores routing overrides.
|
|
545
|
+
Competing provider changes are serialized. During a switch, prompt admission,
|
|
546
|
+
session lifecycle changes, and config changes fail with a retryable `-32600` error.
|
|
547
|
+
A switch also fails if lifecycle/config work or a turn is already active.
|
|
548
|
+
Overlapping lifecycle/config operations on the same session and prompts during
|
|
549
|
+
those operations are rejected with `-32600`; retry after the operation finishes. Inference
|
|
550
|
+
turns do not hold a global lifecycle lock.
|
|
551
|
+
|
|
552
|
+
On failure, the adapter retains the previous committed routing and attempts remote
|
|
553
|
+
compensation for every attempted session, including the request that failed.
|
|
554
|
+
The error data includes `attemptedSessions` and `staleSessions`. Failed compensation
|
|
555
|
+
marks affected sessions with `_meta.codex.routing.stale: true` on a standard
|
|
556
|
+
`config_option_update`; their prompts are rejected until a successful resume or
|
|
557
|
+
provider switch. Successful switching emits `stale: false`. The config update does
|
|
558
|
+
not contain or attest to a provider URL. JSON-RPC errors do not imply no remote side
|
|
559
|
+
effects; uncertain remote state is reported explicitly.
|
|
560
|
+
|
|
561
|
+
Wire logging redacts structured credential/header fields and complete `env` maps,
|
|
562
|
+
including arbitrarily named MCP environment variables. Logs are not guaranteed to
|
|
563
|
+
be secret-free: command arguments, free-form tool output, and Codex stderr may
|
|
564
|
+
contain sensitive text and are not heuristically redacted.
|
|
565
|
+
|
|
566
|
+
A timed-out unsubscribe fences subsequent resume/fork of that thread until the
|
|
567
|
+
outstanding RPC settles. These attempts fail promptly with `-32600`, rather than
|
|
568
|
+
racing a late unsubscribe against a new subscription. Delete/archive requests
|
|
569
|
+
after local close still wait for Codex to acknowledge the remote mutation; the
|
|
570
|
+
local close deadline is not a deadline for those separate remote operations.
|
|
571
|
+
|
|
572
|
+
### Recovery and terminal outcomes
|
|
573
|
+
|
|
574
|
+
Live `agent_message` completion updates carry the authoritative full content for
|
|
575
|
+
that message id. ACP v2 clients replace previously accumulated content when a
|
|
576
|
+
concrete `content` array arrives; they must not append the snapshot to earlier
|
|
577
|
+
`agent_message_chunk` text. History replay uses the same snapshot mapping.
|
|
578
|
+
Completed-only tools, and progress/patch events received without a start, emit a
|
|
579
|
+
complete first upsert with `name`, `title`, and `kind`.
|
|
580
|
+
|
|
581
|
+
Before an idle update, unfinished tool calls are reconciled to the enclosing
|
|
582
|
+
turn's outcome and marked `_meta.codex.reconciled: true`. Orphan terminals receive
|
|
583
|
+
an `exitStatus` with unknown (`null`) exit code and signal; this does not assert
|
|
584
|
+
that a process exited successfully or received a particular OS signal. Their
|
|
585
|
+
`_meta.codex.turnOutcome` records `completed`, `failed`, or `cancelled`.
|
|
586
|
+
|
|
587
|
+
Terminal snapshots retain full output. For all commands, duplicate
|
|
588
|
+
`tool_call_update.rawOutput.output` is capped at 16,384 UTF-16 code units, without
|
|
589
|
+
splitting a surrogate pair. A capped result has `_meta.codex.outputTruncated`,
|
|
590
|
+
`outputCharacters`, and (for terminal-backed commands) `terminalId`; obtain the
|
|
591
|
+
complete output from the terminal or the classified tool call’s text content.
|
|
592
|
+
|
|
593
|
+
Context-window exhaustion ends with `max_tokens`; Codex policy violations end
|
|
594
|
+
with `refusal`. Other failures retain `error`. Error metadata under
|
|
595
|
+
`_meta.codex.error` includes `category` and `retryable`; retryability is advisory
|
|
596
|
+
and never causes automatic resubmission of a failed model turn.
|
|
597
|
+
|
|
598
|
+
A rejected steer becomes a new prompt only after a matching completion or local
|
|
599
|
+
turn finalization proves the old turn ended. Unexpected `turn/started` events are
|
|
600
|
+
observed as running foreground work, including cancellation and finalization.
|
|
601
|
+
Resuming an active thread discovers its current turn through paged history.
|
|
602
|
+
|
|
603
|
+
Repeated cursors in automatically paginated model catalogs or history fail with
|
|
604
|
+
an actionable error. Failed-open cleanup has the same bounded unsubscribe wait
|
|
605
|
+
and pending-unsubscribe fence as session close.
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
### Cache freshness and cancellation
|
|
609
|
+
|
|
610
|
+
Skill metadata is reused for an unchanged working-directory context. A Codex
|
|
611
|
+
`skills/changed` notification invalidates it. Commands refresh immediately for the
|
|
612
|
+
current root context; other sessions refresh on their next foreground prompt.
|
|
613
|
+
Notifications never switch roots, preventing delayed watcher echoes from causing
|
|
614
|
+
a refresh loop.
|
|
615
|
+
Because extra skill roots are process-global in Codex, context changes are
|
|
616
|
+
serialized through thread/turn start acknowledgement; model inference can run
|
|
617
|
+
concurrently. Switching roots invalidates the previous snapshot. Model catalogs
|
|
618
|
+
are shared for up to 30 seconds, invalidated on `account/updated` and local login/logout, and forcibly
|
|
619
|
+
refreshed for provider changes. Account notifications also refresh live session
|
|
620
|
+
account-dependent settings.
|
|
621
|
+
|
|
622
|
+
Request cancellation for `session/new`, `session/resume`, and `session/fork`
|
|
623
|
+
returns `-32800` promptly. A remote request already in flight may still finish;
|
|
624
|
+
its late result is unsubscribed rather than left as an inaccessible open session.
|
|
625
|
+
Lifecycle admission stays held until that operation settles, so a replacement or
|
|
626
|
+
provider transition cannot race the cleanup. Cancellation is not a rollback of
|
|
627
|
+
already completed remote actions.
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
Skill-root serialization covers foreground turns started by this adapter and
|
|
631
|
+
steering into those turns. It is not thread or tenant isolation. Codex-created
|
|
632
|
+
subagent/automatic turns and turns started by other app-server clients snapshot
|
|
633
|
+
process-global roots at their own start time and are outside this guarantee.
|
|
634
|
+
The pinned Codex 0.153 implementation constructs the skill snapshot before
|
|
635
|
+
returning a started turn: [turn input handling](https://github.com/openai/codex/blob/rust-v0.158.0/codex-rs/core/src/session/turn_input.rs)
|
|
636
|
+
and [turn context construction](https://github.com/openai/codex/blob/rust-v0.158.0/codex-rs/core/src/session/turn_context.rs).
|
|
637
|
+
|
|
638
|
+
Turn-scoped `item/*` notifications are ignored while idle or when they name a
|
|
639
|
+
known different active turn. Items are accepted while a local turn's id is still
|
|
640
|
+
unknown, because Codex may deliver them before its start response.
|
|
641
|
+
# Module-owned client tools
|
|
642
|
+
|
|
643
|
+
The adapter advertises `capabilities._meta.alwith.tools = {version: 1}`. On
|
|
644
|
+
`session/new`, `session/resume` and `session/fork`, a trusted host may supply
|
|
645
|
+
`_meta.alwith.tools = {version: 1, revision, definitions}`. Definitions contain
|
|
646
|
+
unique `name`, `description` and object `inputSchema` fields (at most 128 tools).
|
|
647
|
+
New threads receive these as Codex dynamic functions before their first turn.
|
|
648
|
+
Codex persists dynamic definitions across resume/fork; the host must retain the
|
|
649
|
+
original revision and declarations with the session and refuse incompatible
|
|
650
|
+
restores. Supplying a new set on resume does not replace persisted definitions.
|
|
651
|
+
|
|
652
|
+
Calls use the reverse request `_alwith/tool/call` with
|
|
653
|
+
`{sessionId, turnId, toolCallId, name, arguments, toolSetRevision}`. Tool names in
|
|
654
|
+
Codex have an `alwith_client_` prefix; callback names are the original names.
|
|
655
|
+
Results are `{success, contentItems}`. Items are `{type:"text",text}` or bounded
|
|
656
|
+
base64 `{type:"image"|"audio",mimeType,data}`. Arbitrary resource URLs are never
|
|
657
|
+
fetched by the adapter. Results have a 16 MiB serialized limit.
|
|
658
|
+
|
|
659
|
+
Declaring a tool does not grant permission. Non-full-access modes use the
|
|
660
|
+
existing ACP permission channel before execution. Only this user decision
|
|
661
|
+
enters `requires_action`; executing the host callback remains `running`.
|
|
662
|
+
The module must still validate scope, arguments, task ownership and revision.
|
|
663
|
+
Cancellation uses standard ACP `$/cancel_request` with `requestId`. Callbacks
|
|
664
|
+
are deduplicated within a turn; conflicting call IDs fail closed. Cancel,
|
|
665
|
+
session close and engine disconnect terminate waiting, and late results cannot
|
|
666
|
+
update a subsequent turn. Hosts must separately preserve operation identity
|
|
667
|
+
for writes whose outcome is unknown across process loss.
|
|
668
|
+
|
|
669
|
+
Runtime hosts route callbacks privately to the module that owns the Agent
|
|
670
|
+
process. They must not expose tool arguments/results on the public event bus
|
|
671
|
+
or replay journal. UI progress uses normal `tool_call_update` frames.
|
|
672
|
+
|
|
673
|
+
### Host session instructions and initial mode
|
|
674
|
+
|
|
675
|
+
`session/new`, `session/resume`, and `session/fork` accept
|
|
676
|
+
`_meta.alwith.appendSystemPrompt` (a string) as Codex `developerInstructions`.
|
|
677
|
+
The adapter preserves Codex base instructions; host text is never injected as a user message.
|
|
678
|
+
`_meta.codex.mode` explicitly selects `read-only`, `agent`, or `agent-full-access`
|
|
679
|
+
for that session, overriding the process default. Unknown modes are rejected before opening a thread.
|
|
680
|
+
|
|
681
|
+
Empty-preview fork compatibility: Codex 0.156.1 omits such threads from `thread/list`
|
|
682
|
+
even after continuation. For `session/list` the adapter reads only
|
|
683
|
+
`session_meta` headers beneath the server-reported `codexHome` (`sessions` or
|
|
684
|
+
`archived_sessions`), discovers native `forked_from_id` records, and resolves their
|
|
685
|
+
summaries with `thread/read`. The hydrated preview from `thread/read` can differ from the list index; the adapter
|
|
686
|
+
checks every native page before supplementing IDs to avoid duplicates. Only missing forks from the requested
|
|
687
|
+
source/archive/project scope are appended. Native cursors are retained. Native list rows can also omit `forkedFromId` after
|
|
688
|
+
unload; each page is enriched with the lineage confirmed by `thread/read`.
|
|
689
|
+
No Codex file is written and no second conversation index is persisted. This also
|
|
690
|
+
allows a fresh adapter process to discover forks before they have a new user turn.
|
|
691
|
+
|
|
692
|
+
Fork/resume responses with history replay also include `_meta.codex.forkedAtTurnId`
|
|
693
|
+
(string or null) for forks. This is the last native turn ID shared with the parent,
|
|
694
|
+
resolved from both native histories each time; it identifies where an inherited-history
|
|
695
|
+
marker belongs even after the child continues and the adapter restarts. Null means
|
|
696
|
+
there is no shared visible turn (for example an empty fork).
|
|
697
|
+
|
|
698
|
+
### Interrupted history and inline images
|
|
699
|
+
|
|
700
|
+
History replay updates from an interrupted Codex turn carry
|
|
701
|
+
`_meta.codex.historyInterrupted: true`, preserving the turn ID and other metadata.
|
|
702
|
+
This describes the persisted turn status; it does not synthesize missing output.
|
|
703
|
+
Inline base64 image inputs replay as ACP image blocks. Remote image URLs and
|
|
704
|
+
file IDs replay as resource links, without fetching their contents.
|
|
705
|
+
|
|
706
|
+
### File-reference replay and fallback titles
|
|
707
|
+
|
|
708
|
+
Standalone `[@name](file://...)` text blocks written by the adapter replay as ACP
|
|
709
|
+
`resource_link` blocks, including existing histories. Inline Markdown links and
|
|
710
|
+
embedded resource context remain text. Native mention inputs also replay as
|
|
711
|
+
resource links. No sidecar transcript or file-content fetch is involved.
|
|
712
|
+
Fallback titles prefer the user's text over file references; attachment-only
|
|
713
|
+
prompts use the first resource name. Explicit Codex thread names are preserved.
|
|
714
|
+
Session listing reads the first history page for unnamed attachment-prefixed
|
|
715
|
+
previews, including truncated previews, so list and replay titles agree.
|
|
716
|
+
|
|
717
|
+
Live interrupted turns upsert their last user message with
|
|
718
|
+
`_meta.codex.turnInterrupted: true`. This remains attached to that prompt after
|
|
719
|
+
another turn starts; it must not be inferred from the currently selected chat or
|
|
720
|
+
from a later turn's stop reason. Native history replay continues to use
|
|
721
|
+
`historyInterrupted` with its existing incomplete-history explanation.
|
|
722
|
+
|
|
723
|
+
### Native image attachment replay
|
|
724
|
+
|
|
725
|
+
Native `localImage` user inputs are replayed as inline ACP `image` blocks, including
|
|
726
|
+
attachments outside the thread directory. Only native attachment references grant
|
|
727
|
+
read access; Markdown links and client-supplied metadata do not. Reads are async,
|
|
728
|
+
limited to regular raster image files up to 32 MiB per image, and do not create a
|
|
729
|
+
second history store. Missing, oversized or unsupported files become a
|
|
730
|
+
`resource_link` with URI `codex-image:unavailable` and
|
|
731
|
+
`_meta.codex.imageUnavailable: true`; clients should show a non-interactive image
|
|
732
|
+
placeholder without displaying local paths.
|
|
733
|
+
|
|
734
|
+
A complete Desktop image attachment envelope is removed from display text only
|
|
735
|
+
when every declared filename/path is well formed, all native local-image paths
|
|
736
|
+
match, and the remaining entries match the number of native inline Base64 images.
|
|
737
|
+
This normalization never reads a path mentioned only in the envelope. The
|
|
738
|
+
text after `## My request:` becomes the user body. Ordinary text and incomplete or
|
|
739
|
+
mismatched envelopes remain unchanged. Raw history export retains native input.
|
|
740
|
+
|
|
741
|
+
History replay updates carry `_meta.codex.historyReplay: true`. When Codex provides
|
|
742
|
+
`durationMs` (or both start and completion timestamps), updates also carry
|
|
743
|
+
`_meta.codex.turnDurationMs` in milliseconds. Clients must not derive historical
|
|
744
|
+
work duration from replay arrival times; when native timing is absent, show a
|
|
745
|
+
non-numeric completed-work label.
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
### Message clocks and work duration
|
|
749
|
+
|
|
750
|
+
Live user-message upserts preserve the client-recorded `_meta.codex.messageSentAt`
|
|
751
|
+
from the prompt request; clients without it use the native item's `startedAtMs`. Final assistant snapshots carry `messageCompletedAt` from the native
|
|
752
|
+
item's `completedAtMs`. `turnCompletedAt` is the native turn completion in Unix
|
|
753
|
+
milliseconds, used for restored replies when item-level completion is unavailable.
|
|
754
|
+
The opening historical user message uses the native turn submission/start time;
|
|
755
|
+
subsequent user messages in the same historical turn omit a timestamp because the
|
|
756
|
+
history API does not expose their individual send times.
|
|
757
|
+
|
|
758
|
+
Live `workStartedAt` uses the opening prompt's recorded send time when provided,
|
|
759
|
+
otherwise the native turn start. The first synchronous final-answer
|
|
760
|
+
item start seals `workDurationMs`; streaming the final answer does not extend it.
|
|
761
|
+
These fields are routed by turn ID and retained on normal message upserts.
|
|
762
|
+
Native history does not expose this work boundary, so it only carries
|
|
763
|
+
`turnDurationMs`. Clients label that value as total time, never as work time.
|
|
764
|
+
Missing timing remains absent; replay arrival times are not substitutes.
|
|
765
|
+
|
|
766
|
+
### Desktop questionnaire replies
|
|
767
|
+
|
|
768
|
+
A complete `send_user_message_question_reply` envelope containing a non-empty
|
|
769
|
+
array of `{questionItemId, question, answer}` strings is replayed as ordered ACP
|
|
770
|
+
text blocks. Each block contains readable `question\nanswer` text and
|
|
771
|
+
`_meta.codex.questionReply: {questionItemId, question, answer}`. Clients may render
|
|
772
|
+
this metadata as a single-line question summary above the literal answer. Copy
|
|
773
|
+
and search retain readable text. Native history remains unchanged. Malformed,
|
|
774
|
+
partial, or quoted envelopes remain ordinary text; no entries are silently lost.
|
|
775
|
+
For locally submitted envelopes, message-level `_meta.codex.questionReplyParts`
|
|
776
|
+
also contains ordered `{text, question?}` display parts. Receipt-adopting clients
|
|
777
|
+
that retain optimistic prompt content use these parts for display and copying;
|
|
778
|
+
this does not alter the original input sent to Codex.
|
|
779
|
+
|
|
780
|
+
Review results: when `exitedReviewMode.review` repeats a non-commentary agent message
|
|
781
|
+
in the same turn, live updates and full-turn history omit the duplicate exit message.
|
|
782
|
+
Review exits are buffered until the terminal drain to cover either native event order.
|
|
783
|
+
A standalone or different review result remains visible. Item-level history retains
|
|
784
|
+
its per-item projection because the matching answer may be outside the requested page.
|
|
785
|
+
|
|
786
|
+
### Plugin enablement
|
|
787
|
+
|
|
788
|
+
`capabilities._meta.codex.pluginEnable: true` advertises `_codex/plugin_set_enabled`.
|
|
789
|
+
For local plugins the adapter upserts only the requested plugin's `enabled` field
|
|
790
|
+
through Codex's config API. For remote plugins clients must also pass the catalog's
|
|
791
|
+
`remotePluginId`. The adapter calls ChatGPT plugin-service's enable/disable endpoint
|
|
792
|
+
using credentials supplied by Codex, then calls `plugin/reconcile` and verifies
|
|
793
|
+
the installed catalog. Remote state overrides local config in Codex. Credentials
|
|
794
|
+
never cross the ACP boundary. It rejects malformed inputs and reports an
|
|
795
|
+
error if a higher-priority configuration overrides a local write. It emits
|
|
796
|
+
`_codex/skills_changed` after a saved change so clients can reload the effective catalog.
|
|
797
|
+
Remote plugin read/install requests should pass the catalog's `remotePluginId`
|
|
798
|
+
as `pluginName` when present; generated app aliases are not remote service IDs.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.7.
|
|
6
|
+
"version": "0.7.12",
|
|
7
7
|
"description": "Native ACP v2 agent for the OpenAI Codex app-server",
|
|
8
8
|
"packageManager": "bun@1.4.0",
|
|
9
9
|
"engines": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"CHANGELOG.md",
|
|
19
19
|
"LICENSE",
|
|
20
|
-
"package.json"
|
|
20
|
+
"package.json",
|
|
21
|
+
"docs/protocol.md"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"prepublishOnly": "bun run build",
|