@jskit-ai/assistant-core 0.1.199 → 0.1.201
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,2710 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CodexAppServerJsonRpcClient } from "./codexClient.js";
|
|
5
|
+
import {
|
|
6
|
+
createCodexAccountReader, codexAuthOutputRequiresReconnect, codexAppServerProjectHookTrustConfig,
|
|
7
|
+
codexAppServerTextHasControlCharacters, CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH
|
|
8
|
+
} from "./codexConfiguration.js";
|
|
9
|
+
import {
|
|
10
|
+
CODEX_APP_SERVER_PROCESS_STATE, CODEX_APP_SERVER_TRANSPORT,
|
|
11
|
+
assertExistingDirectory, codexAppServerCommandBaseEnv, codexAppServerCredentialHome,
|
|
12
|
+
codexAppServerExecutionMode, codexAppServerHelperWorkspaceDir, codexAppServerIsHelper,
|
|
13
|
+
codexAppServerProcessCwd, codexAppServerRuntimeIdentity, codexAppServerTerminalEnvHash,
|
|
14
|
+
ensureWritablePrivateDirectory, normalizeCodexAppServerTerminalEnv, tailTextFile
|
|
15
|
+
} from "./codexProcess.js";
|
|
16
|
+
import { prepareCodexHistory } from "./codexHistoryAdapter.js";
|
|
17
|
+
import { createCodexConversationAdapter } from "./providers/codex.js";
|
|
18
|
+
import { exportCodexNativeHistory } from "./codexNativeHistoryExport.js";
|
|
19
|
+
import { retireNativeConversation } from "./nativeHistoryExport.js";
|
|
20
|
+
import { isPlainObject, normalizeText as normalizeAgentText } from "./normalize.js";
|
|
21
|
+
import {
|
|
22
|
+
codexAppServerProviderThreadTurns,
|
|
23
|
+
codexAppServerProviderTurnItems,
|
|
24
|
+
codexAppServerProviderTurnClientIds,
|
|
25
|
+
codexAppServerThreadRawValue
|
|
26
|
+
} from "./codexEvents.js";
|
|
27
|
+
|
|
28
|
+
const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
|
|
29
|
+
const CODEX_APP_SERVER_EXECUTION_MODES = Object.freeze({ HELPER: "helper", INTERACTIVE: "interactive" });
|
|
30
|
+
const codexAppServerHelperAuthError = createCodexAccountReader().error;
|
|
31
|
+
|
|
32
|
+
const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
|
|
33
|
+
const CODEX_RENEWAL_UNMATERIALIZED_THREAD_SUFFIX =
|
|
34
|
+
"is not materialized yet; includeTurns is unavailable before first user message";
|
|
35
|
+
const CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT = 100;
|
|
36
|
+
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES = 100;
|
|
37
|
+
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRIES = 1000;
|
|
38
|
+
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRY_BYTES = 32 * 1024;
|
|
39
|
+
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
40
|
+
const CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH = 512;
|
|
41
|
+
const CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT = 100;
|
|
42
|
+
const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_PAGES = 100;
|
|
43
|
+
const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT = 1000;
|
|
44
|
+
const CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH = 512;
|
|
45
|
+
const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_ENTRY_BYTES = 64 * 1024;
|
|
46
|
+
const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
47
|
+
const CODEX_APP_SERVER_HELPER_MAX_MESSAGE_BYTES = 4 * 1024 * 1024;
|
|
48
|
+
const CODEX_APP_SERVER_SERVER_INFO_USER_AGENT_MAX_LENGTH = 512;
|
|
49
|
+
const CODEX_AUTH_PREFLIGHT_TIMEOUT_MS = 15000;
|
|
50
|
+
const CODEX_AUTH_PREFLIGHT_OUTPUT_TAIL_BYTES = 4096;
|
|
51
|
+
const CODEX_APP_SERVER_CHATGPT_REFRESH_METHOD = "account/chatgptAuthTokens/refresh";
|
|
52
|
+
const CODEX_NATIVE_STORAGE_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview",
|
|
53
|
+
"subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"];
|
|
54
|
+
|
|
55
|
+
function normalizePositiveInteger(value, fallback) {
|
|
56
|
+
const number = Number(value);
|
|
57
|
+
return Number.isSafeInteger(number) && number > 0 ? number : fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function codexAppServerRequestIsInvalid(error = null, method = "") {
|
|
61
|
+
const expectedMethod = normalizeAgentText(method);
|
|
62
|
+
const actualMethod = normalizeAgentText(error?.method);
|
|
63
|
+
return Number(error?.code) === CODEX_APP_SERVER_INVALID_REQUEST_CODE &&
|
|
64
|
+
(!expectedMethod || !actualMethod || actualMethod === expectedMethod);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function codexAppServerThreadIsMissing(error = null, threadId = "") {
|
|
68
|
+
const normalizedThreadId = normalizeAgentText(threadId || "").toLowerCase();
|
|
69
|
+
if (
|
|
70
|
+
!normalizedThreadId ||
|
|
71
|
+
!codexAppServerRequestIsInvalid(error, "thread/read")
|
|
72
|
+
) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// Codex also uses -32600 for unrelated invalid requests, so recovery must
|
|
76
|
+
// match both the missing-thread wording and the exact durable thread id.
|
|
77
|
+
const message = normalizeAgentText(error?.message || "").toLowerCase();
|
|
78
|
+
return [
|
|
79
|
+
`thread not loaded: ${normalizedThreadId}`,
|
|
80
|
+
`thread ${normalizedThreadId} not found`,
|
|
81
|
+
`no rollout found for thread id ${normalizedThreadId}`
|
|
82
|
+
].includes(message);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function retireCodexConversationHistory(provider, binding, {
|
|
86
|
+
toolHomeSource, beforeDelete, signal, errorPrefix = ""
|
|
87
|
+
} = {}) {
|
|
88
|
+
const inspect = async () => {
|
|
89
|
+
const ids = [...new Set([binding.conversationId, ...await provider.listThreadDescendants(binding.conversationId)])].sort();
|
|
90
|
+
const records = [];
|
|
91
|
+
for (const conversationId of ids) {
|
|
92
|
+
let thread;
|
|
93
|
+
try { thread = codexAppServerThreadRawValue(await provider.readThreadStatus(conversationId)); }
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Native thread/read also says "not loaded" after deletion. Only
|
|
96
|
+
// an exhaustive native inventory can distinguish absence from an
|
|
97
|
+
// existing unloaded thread, including one moved to another cwd.
|
|
98
|
+
if (codexAppServerThreadIsMissing(error, conversationId) &&
|
|
99
|
+
!await provider.nativeThreadExists(conversationId, { signal })) continue;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
if (thread.id !== conversationId || thread.cwd !== binding.workdir ||
|
|
103
|
+
!["idle", "notLoaded"].includes(thread.status?.type)) {
|
|
104
|
+
throw new Error("Codex retirement requires an idle native family in the exact saved directory.");
|
|
105
|
+
}
|
|
106
|
+
if (thread.historyMode !== "paginated") {
|
|
107
|
+
throw Object.assign(new Error("Codex native retirement requires paginated history. This unsupported conversation needs operator cleanup."),
|
|
108
|
+
{ code: `${errorPrefix}codex_paginated_history_required` });
|
|
109
|
+
}
|
|
110
|
+
const relative = path.isAbsolute(thread.path || "") && toolHomeSource
|
|
111
|
+
? path.relative(path.join(toolHomeSource, ".codex"), thread.path) : "";
|
|
112
|
+
if (thread.path && (!/^(?:sessions|archived_sessions)\//u.test(relative) || !/\.jsonl(?:\.zst)?$/u.test(thread.path))) {
|
|
113
|
+
throw new Error("Codex returned an unsafe or unknown rollout path.");
|
|
114
|
+
}
|
|
115
|
+
const files = [];
|
|
116
|
+
const plainPath = thread.path?.replace(/\.zst$/u, "");
|
|
117
|
+
for (const file of plainPath ? [plainPath, `${plainPath}.zst`] : []) {
|
|
118
|
+
try {
|
|
119
|
+
const info = await lstat(file);
|
|
120
|
+
if (!info.isFile() || info.isSymbolicLink() || await realpath(file) !== path.resolve(file)) {
|
|
121
|
+
throw new Error("Codex rollout is not a regular file at its exact native path.");
|
|
122
|
+
}
|
|
123
|
+
files.push({ path: file, inode: info.ino, size: info.size, modified: info.mtimeMs });
|
|
124
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
125
|
+
}
|
|
126
|
+
records.push({ conversationId, workdir: thread.cwd, historyMode: thread.historyMode, ...(files[0] || {}), files,
|
|
127
|
+
nativePath: thread.path ?? null, status: thread.status.type,
|
|
128
|
+
...(Number.isFinite(thread.createdAt) ? { createdAt: new Date(thread.createdAt * 1000).toISOString() } : {}),
|
|
129
|
+
...(Number.isFinite(thread.updatedAt) ? { updatedAt: new Date(thread.updatedAt * 1000).toISOString() } : {}) });
|
|
130
|
+
}
|
|
131
|
+
return records;
|
|
132
|
+
};
|
|
133
|
+
return retireNativeConversation({ binding, inspect, beforeDelete,
|
|
134
|
+
exportConversation: (id, onRecord) => provider.exportThreadHistory(id, onRecord, { signal }),
|
|
135
|
+
remove: () => provider.deleteThread(binding.conversationId) });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function codexAppServerThreadHasReadableHistory(provider = null, threadId = "") {
|
|
139
|
+
const normalizedThreadId = normalizeAgentText(threadId);
|
|
140
|
+
if (!normalizedThreadId || typeof provider?.readThread !== "function") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await provider.readThread(normalizedThreadId);
|
|
145
|
+
return true;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (codexAppServerRequestIsInvalid(error, "thread/read")) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The caller has already checked the requested thread against its current
|
|
155
|
+
// conversation. History can confirm this exact authored input, never infer it
|
|
156
|
+
// from an assistant item or from a failed read.
|
|
157
|
+
async function inspectCodexAppServerMessageAdmission({ provider, threadId, messageId } = {}) {
|
|
158
|
+
try {
|
|
159
|
+
const thread = await provider.readThread(threadId);
|
|
160
|
+
const accepted = codexAppServerProviderThreadTurns(thread).find((turn) =>
|
|
161
|
+
codexAppServerProviderTurnItems(turn).some((item) => item.type === "userMessage" &&
|
|
162
|
+
codexAppServerProviderTurnClientIds({ items: [item] }).includes(messageId)));
|
|
163
|
+
return { ok: true, admission: accepted ? "accepted" : "unknown", messageId, threadId,
|
|
164
|
+
turnId: normalizeAgentText(accepted?.id) };
|
|
165
|
+
} catch {
|
|
166
|
+
return { ok: true, admission: "unknown", messageId, threadId };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Native result validation only. The caller owns any interruption, absence
|
|
171
|
+
// proof and durable cleanup receipt.
|
|
172
|
+
async function deleteCodexAppServerThread({ provider, threadId } = {}) {
|
|
173
|
+
const result = await provider.deleteThread(threadId);
|
|
174
|
+
if (!isPlainObject(result)) {
|
|
175
|
+
const error = new Error("Codex app-server returned an invalid thread deletion result.");
|
|
176
|
+
error.code = "codex_helper_thread_delete_unconfirmed";
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The caller persists cleanup ownership before this operation and removes that
|
|
183
|
+
// receipt only after success. Native failure details remain local to that owner.
|
|
184
|
+
async function deleteCodexAppServerHelperThread({ provider, threadId, turnId = "" } = {}) {
|
|
185
|
+
let interruptError = null;
|
|
186
|
+
if (turnId) {
|
|
187
|
+
if (typeof provider?.interruptTurn === "function") {
|
|
188
|
+
try {
|
|
189
|
+
await provider.interruptTurn(threadId, turnId);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
interruptError = error;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
interruptError = new Error("Codex provider cannot interrupt an active helper turn.");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (typeof provider?.deleteThread !== "function") {
|
|
198
|
+
return { ok: false, error: new Error("Codex provider cannot delete a helper thread."), interruptError };
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const result = await deleteCodexAppServerThread({ provider, threadId });
|
|
202
|
+
return {
|
|
203
|
+
deleted: true,
|
|
204
|
+
interrupted: Boolean(turnId) && !interruptError,
|
|
205
|
+
ok: true,
|
|
206
|
+
result,
|
|
207
|
+
status: "deleted",
|
|
208
|
+
threadId,
|
|
209
|
+
turnId,
|
|
210
|
+
interruptError
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
let absent = false;
|
|
214
|
+
if (codexAppServerRequestIsInvalid(error, "thread/delete")) {
|
|
215
|
+
try {
|
|
216
|
+
absent = !await codexAppServerThreadHasReadableHistory(provider, threadId);
|
|
217
|
+
} catch {
|
|
218
|
+
absent = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (absent) {
|
|
222
|
+
return {
|
|
223
|
+
deleted: false,
|
|
224
|
+
interrupted: Boolean(turnId) && !interruptError,
|
|
225
|
+
ok: true,
|
|
226
|
+
status: "notFound",
|
|
227
|
+
threadId,
|
|
228
|
+
turnId,
|
|
229
|
+
interruptError
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return { ok: false, error, interruptError };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Retained thread selection keeps observation ahead of a resume that can start work. */
|
|
237
|
+
async function ensureCodexAppServerThread({
|
|
238
|
+
observeThread,
|
|
239
|
+
provider,
|
|
240
|
+
settings,
|
|
241
|
+
projectHooks = false,
|
|
242
|
+
identity,
|
|
243
|
+
workdir = "",
|
|
244
|
+
onStage = () => {}
|
|
245
|
+
} = {}) {
|
|
246
|
+
if (typeof observeThread !== "function") {
|
|
247
|
+
throw new TypeError("Main Codex threads require an observer before they can resume.");
|
|
248
|
+
}
|
|
249
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
250
|
+
let stageStartedAt = Date.now();
|
|
251
|
+
const availability = typeof provider.ensureAvailable === "function"
|
|
252
|
+
? await provider.ensureAvailable()
|
|
253
|
+
: null;
|
|
254
|
+
const appServerRuntime = availability?.runtime || await provider.ensureRuntime();
|
|
255
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: "runtime" });
|
|
256
|
+
const existingThreadId = identity.read(normalizedWorkdir);
|
|
257
|
+
let config = null;
|
|
258
|
+
if (projectHooks) {
|
|
259
|
+
stageStartedAt = Date.now();
|
|
260
|
+
config = await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir);
|
|
261
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: "hook-config" });
|
|
262
|
+
}
|
|
263
|
+
const { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
|
|
264
|
+
let thread = null;
|
|
265
|
+
stageStartedAt = Date.now();
|
|
266
|
+
if (existingThreadId) {
|
|
267
|
+
if (identity.pauseGoal) {
|
|
268
|
+
const { goal } = await provider.readGoal(existingThreadId);
|
|
269
|
+
if (goal?.status === "active") await provider.setGoalStatus(existingThreadId, "paused");
|
|
270
|
+
}
|
|
271
|
+
// Resuming can immediately start an active goal before the RPC returns.
|
|
272
|
+
await observeThread(existingThreadId);
|
|
273
|
+
thread = await provider.resumeThread(existingThreadId, threadSettings);
|
|
274
|
+
if (identity.pauseGoal) {
|
|
275
|
+
await identity.clearPause();
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
thread = await provider.startThread(threadStartSettings);
|
|
279
|
+
}
|
|
280
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: existingThreadId ? "resume" : "start" });
|
|
281
|
+
const threadId = normalizeAgentText(thread.id || existingThreadId);
|
|
282
|
+
if (!threadId) {
|
|
283
|
+
throw new Error("Codex app-server did not return a thread id.");
|
|
284
|
+
}
|
|
285
|
+
stageStartedAt = Date.now();
|
|
286
|
+
await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir });
|
|
287
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: "identity-metadata" });
|
|
288
|
+
return {
|
|
289
|
+
appServerRuntime,
|
|
290
|
+
thread,
|
|
291
|
+
threadId
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function defineCodexRenewalThreadIds(value = [], {
|
|
296
|
+
errorCode = "codex_renewal_fresh_thread_required"
|
|
297
|
+
} = {}) {
|
|
298
|
+
if (
|
|
299
|
+
!Array.isArray(value) ||
|
|
300
|
+
value.length > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT
|
|
301
|
+
) {
|
|
302
|
+
throw codexRenewalThreadError(
|
|
303
|
+
errorCode,
|
|
304
|
+
"The successor assistant thread inventory is invalid."
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const threadIds = value.map((threadId) => normalizeAgentText(threadId));
|
|
308
|
+
if (
|
|
309
|
+
threadIds.some((threadId) => (
|
|
310
|
+
!threadId ||
|
|
311
|
+
threadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
|
|
312
|
+
codexAppServerTextHasControlCharacters(threadId)
|
|
313
|
+
)) ||
|
|
314
|
+
new Set(threadIds).size !== threadIds.length
|
|
315
|
+
) {
|
|
316
|
+
throw codexRenewalThreadError(
|
|
317
|
+
errorCode,
|
|
318
|
+
"The successor assistant thread inventory contains an invalid identity."
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return Object.freeze([...threadIds].sort());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function listCodexRenewalThreadIds(provider, workdir, errorCode) {
|
|
325
|
+
if (typeof provider?.listAppServerThreadsForCwd !== "function") {
|
|
326
|
+
throw codexRenewalThreadError(
|
|
327
|
+
errorCode,
|
|
328
|
+
"The assistant provider cannot inventory the successor's exact session threads."
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
332
|
+
const inventory = await provider.listAppServerThreadsForCwd({
|
|
333
|
+
cwd: normalizedWorkdir
|
|
334
|
+
});
|
|
335
|
+
if (normalizeAgentText(inventory?.cwd) !== normalizedWorkdir) {
|
|
336
|
+
throw codexRenewalThreadError(
|
|
337
|
+
errorCode,
|
|
338
|
+
"The assistant provider returned a thread inventory for a different session source."
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return defineCodexRenewalThreadIds(inventory?.threadIds, { errorCode });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function assertCodexRenewalThreadSnapshot(threadSnapshot = null, {
|
|
345
|
+
threadId = "",
|
|
346
|
+
workdir = "",
|
|
347
|
+
errorCode
|
|
348
|
+
} = {}) {
|
|
349
|
+
const expectedThreadId = normalizeAgentText(threadId);
|
|
350
|
+
const expectedWorkdir = normalizeAgentText(workdir);
|
|
351
|
+
const actualThreadId = codexAppServerThreadResponseId(threadSnapshot);
|
|
352
|
+
const actualWorkdir = normalizeAgentText(
|
|
353
|
+
threadSnapshot?.cwd ||
|
|
354
|
+
threadSnapshot?.raw?.cwd ||
|
|
355
|
+
threadSnapshot?.response?.thread?.cwd
|
|
356
|
+
);
|
|
357
|
+
if (actualThreadId !== expectedThreadId || actualWorkdir !== expectedWorkdir) {
|
|
358
|
+
throw codexRenewalThreadError(
|
|
359
|
+
errorCode,
|
|
360
|
+
"The assistant provider could not verify the exact successor thread and session source.",
|
|
361
|
+
{
|
|
362
|
+
actualThreadId,
|
|
363
|
+
actualWorkdir,
|
|
364
|
+
expectedThreadId,
|
|
365
|
+
expectedWorkdir
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return threadSnapshot;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function codexRenewalThreadNeedsStatusRead(error = null, threadId = "") {
|
|
373
|
+
const normalizedThreadId = normalizeAgentText(threadId);
|
|
374
|
+
return Boolean(
|
|
375
|
+
normalizedThreadId &&
|
|
376
|
+
(
|
|
377
|
+
(
|
|
378
|
+
codexAppServerRequestIsInvalid(error, "thread/read") &&
|
|
379
|
+
normalizeAgentText(error?.message) ===
|
|
380
|
+
`thread ${normalizedThreadId} ${CODEX_RENEWAL_UNMATERIALIZED_THREAD_SUFFIX}`
|
|
381
|
+
) ||
|
|
382
|
+
(
|
|
383
|
+
Number(error?.code) === -32601 &&
|
|
384
|
+
normalizeAgentText(error?.method) === "thread/read"
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function readCodexRenewalSuccessorThreadSnapshot({
|
|
391
|
+
provider,
|
|
392
|
+
threadId = "",
|
|
393
|
+
workdir = "",
|
|
394
|
+
errorCode
|
|
395
|
+
} = {}) {
|
|
396
|
+
try {
|
|
397
|
+
return assertCodexRenewalThreadSnapshot(
|
|
398
|
+
await provider.readThread(threadId),
|
|
399
|
+
{ threadId, workdir, errorCode }
|
|
400
|
+
);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (
|
|
403
|
+
!codexRenewalThreadNeedsStatusRead(error, threadId) ||
|
|
404
|
+
typeof provider?.readThreadStatus !== "function"
|
|
405
|
+
) {
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
return assertCodexRenewalThreadSnapshot(
|
|
409
|
+
await provider.readThreadStatus(threadId),
|
|
410
|
+
{ threadId, workdir, errorCode }
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function codexRenewalThreadError(code, message, details = {}, {
|
|
416
|
+
retryable = false
|
|
417
|
+
} = {}) {
|
|
418
|
+
const error = new Error(message);
|
|
419
|
+
error.code = code;
|
|
420
|
+
error.details = {
|
|
421
|
+
...details,
|
|
422
|
+
retryable
|
|
423
|
+
};
|
|
424
|
+
error.retryable = retryable;
|
|
425
|
+
return error;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function codexAppServerThreadResponseId(thread = null, fallback = "") {
|
|
429
|
+
return normalizeAgentText(
|
|
430
|
+
thread?.id ||
|
|
431
|
+
thread?.response?.thread?.id ||
|
|
432
|
+
fallback
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Resume only the supplied retained thread; native read failures never replace it. */
|
|
437
|
+
async function resumeExactCodexAppServerThread({
|
|
438
|
+
expectedThreadId = "",
|
|
439
|
+
provider,
|
|
440
|
+
settings,
|
|
441
|
+
projectHooks = false,
|
|
442
|
+
workdir = "",
|
|
443
|
+
errorCode = "codex_renewal_thread_unreadable"
|
|
444
|
+
} = {}) {
|
|
445
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
446
|
+
const normalizedExpectedThreadId = normalizeAgentText(expectedThreadId);
|
|
447
|
+
if (
|
|
448
|
+
typeof provider?.ensureRuntime !== "function" ||
|
|
449
|
+
typeof provider?.resumeThread !== "function" ||
|
|
450
|
+
typeof provider?.readThread !== "function"
|
|
451
|
+
) {
|
|
452
|
+
throw codexRenewalThreadError(
|
|
453
|
+
errorCode,
|
|
454
|
+
"The old assistant provider cannot read its exact main thread.",
|
|
455
|
+
{ expectedThreadId: normalizedExpectedThreadId }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const appServerRuntime = await provider.ensureRuntime();
|
|
459
|
+
const config = projectHooks
|
|
460
|
+
? await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir)
|
|
461
|
+
: null;
|
|
462
|
+
const { threadSettings } = await settings(normalizedWorkdir, config);
|
|
463
|
+
let thread = null;
|
|
464
|
+
let threadSnapshot = null;
|
|
465
|
+
try {
|
|
466
|
+
thread = await provider.resumeThread(normalizedExpectedThreadId, threadSettings);
|
|
467
|
+
const resumedThreadId = codexAppServerThreadResponseId(thread, normalizedExpectedThreadId);
|
|
468
|
+
if (resumedThreadId !== normalizedExpectedThreadId) {
|
|
469
|
+
throw codexRenewalThreadError(
|
|
470
|
+
errorCode,
|
|
471
|
+
"The old assistant provider resumed a different thread.",
|
|
472
|
+
{
|
|
473
|
+
expectedThreadId: normalizedExpectedThreadId,
|
|
474
|
+
resumedThreadId
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
threadSnapshot = await provider.readThread(normalizedExpectedThreadId);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (
|
|
481
|
+
error?.code === errorCode ||
|
|
482
|
+
codexAppServerRequestIsInvalid(error, "thread/resume") ||
|
|
483
|
+
codexAppServerRequestIsInvalid(error, "thread/read")
|
|
484
|
+
) {
|
|
485
|
+
if (error?.code === errorCode) {
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
throw codexRenewalThreadError(
|
|
489
|
+
errorCode,
|
|
490
|
+
"The old assistant thread cannot be read. Write or edit the handover manually instead.",
|
|
491
|
+
{
|
|
492
|
+
expectedThreadId: normalizedExpectedThreadId,
|
|
493
|
+
providerError: normalizeAgentText(error?.message)
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
appServerRuntime,
|
|
501
|
+
thread,
|
|
502
|
+
threadId: normalizedExpectedThreadId,
|
|
503
|
+
threadSnapshot,
|
|
504
|
+
threadSettings
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Preserve the original baseline → create/adopt → durable identity → first-read order. */
|
|
509
|
+
async function startFreshCodexAppServerThread({
|
|
510
|
+
provider,
|
|
511
|
+
settings,
|
|
512
|
+
projectHooks = false,
|
|
513
|
+
identity,
|
|
514
|
+
resumableThreadId = "",
|
|
515
|
+
forbiddenThreadId = "",
|
|
516
|
+
operationId = "",
|
|
517
|
+
workdir = "",
|
|
518
|
+
errorCode = "codex_renewal_fresh_thread_required",
|
|
519
|
+
applicationName = "The application"
|
|
520
|
+
} = {}) {
|
|
521
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
522
|
+
const normalizedForbiddenThreadId = normalizeAgentText(forbiddenThreadId);
|
|
523
|
+
const normalizedOperationId = normalizeAgentText(operationId);
|
|
524
|
+
if (resumableThreadId && resumableThreadId === normalizedForbiddenThreadId) {
|
|
525
|
+
throw codexRenewalThreadError(
|
|
526
|
+
errorCode,
|
|
527
|
+
"The renewed session cannot reuse the old session's assistant thread.",
|
|
528
|
+
{ forbiddenThreadId: normalizedForbiddenThreadId }
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (
|
|
532
|
+
typeof provider?.ensureRuntime !== "function" ||
|
|
533
|
+
typeof provider?.resumeThread !== "function" ||
|
|
534
|
+
typeof provider?.readThread !== "function" ||
|
|
535
|
+
typeof provider?.startThread !== "function"
|
|
536
|
+
) {
|
|
537
|
+
throw codexRenewalThreadError(
|
|
538
|
+
errorCode,
|
|
539
|
+
"The assistant provider cannot prove a genuinely fresh renewal thread."
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const appServerRuntime = await provider.ensureRuntime();
|
|
543
|
+
const config = projectHooks
|
|
544
|
+
? await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir)
|
|
545
|
+
: null;
|
|
546
|
+
const { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
|
|
547
|
+
let fresh = false;
|
|
548
|
+
let thread = null;
|
|
549
|
+
let threadSnapshot = null;
|
|
550
|
+
let baselineThreadIds = Object.freeze([]);
|
|
551
|
+
if (resumableThreadId) {
|
|
552
|
+
try {
|
|
553
|
+
thread = await provider.resumeThread(resumableThreadId, threadSettings);
|
|
554
|
+
const resumedThreadId = codexAppServerThreadResponseId(thread, resumableThreadId);
|
|
555
|
+
if (resumedThreadId !== resumableThreadId) {
|
|
556
|
+
throw codexRenewalThreadError(
|
|
557
|
+
errorCode,
|
|
558
|
+
"The assistant provider resumed a different renewal thread.",
|
|
559
|
+
{
|
|
560
|
+
expectedThreadId: resumableThreadId,
|
|
561
|
+
resumedThreadId
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
|
|
566
|
+
provider,
|
|
567
|
+
threadId: resumableThreadId,
|
|
568
|
+
workdir: normalizedWorkdir,
|
|
569
|
+
errorCode
|
|
570
|
+
});
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (
|
|
573
|
+
error?.code === errorCode ||
|
|
574
|
+
codexAppServerRequestIsInvalid(error, "thread/resume") ||
|
|
575
|
+
codexAppServerRequestIsInvalid(error, "thread/read")
|
|
576
|
+
) {
|
|
577
|
+
if (error?.code === errorCode) {
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
throw codexRenewalThreadError(
|
|
581
|
+
errorCode,
|
|
582
|
+
`The previously started renewal thread is no longer readable; ${applicationName} will not replace it silently.`,
|
|
583
|
+
{
|
|
584
|
+
expectedThreadId: resumableThreadId,
|
|
585
|
+
providerError: normalizeAgentText(error?.message)
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
if (!normalizedOperationId) {
|
|
593
|
+
throw codexRenewalThreadError(
|
|
594
|
+
errorCode,
|
|
595
|
+
"Starting a fresh successor thread requires its exact renewal operation id."
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
baselineThreadIds = await identity.readBaseline();
|
|
599
|
+
if (!baselineThreadIds) {
|
|
600
|
+
baselineThreadIds = await listCodexRenewalThreadIds(
|
|
601
|
+
provider,
|
|
602
|
+
normalizedWorkdir,
|
|
603
|
+
errorCode
|
|
604
|
+
);
|
|
605
|
+
await identity.writeBaseline(baselineThreadIds);
|
|
606
|
+
}
|
|
607
|
+
const currentThreadIds = await listCodexRenewalThreadIds(
|
|
608
|
+
provider,
|
|
609
|
+
normalizedWorkdir,
|
|
610
|
+
errorCode
|
|
611
|
+
);
|
|
612
|
+
const baseline = new Set(baselineThreadIds);
|
|
613
|
+
const candidateThreadIds = currentThreadIds.filter((threadId) => !baseline.has(threadId));
|
|
614
|
+
if (candidateThreadIds.length > 1) {
|
|
615
|
+
throw codexRenewalThreadError(
|
|
616
|
+
errorCode,
|
|
617
|
+
`More than one unclaimed assistant thread appeared for this renewal; ${applicationName} will not guess which one is authoritative.`,
|
|
618
|
+
{ candidateThreadIds }
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
if (candidateThreadIds.length === 1) {
|
|
622
|
+
const [candidateThreadId] = candidateThreadIds;
|
|
623
|
+
thread = await provider.resumeThread(candidateThreadId, threadSettings);
|
|
624
|
+
const resumedThreadId = codexAppServerThreadResponseId(thread, candidateThreadId);
|
|
625
|
+
if (resumedThreadId !== candidateThreadId) {
|
|
626
|
+
throw codexRenewalThreadError(
|
|
627
|
+
errorCode,
|
|
628
|
+
"The assistant provider resumed a different recovered renewal thread.",
|
|
629
|
+
{
|
|
630
|
+
expectedThreadId: candidateThreadId,
|
|
631
|
+
resumedThreadId
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
|
|
636
|
+
provider,
|
|
637
|
+
threadId: candidateThreadId,
|
|
638
|
+
workdir: normalizedWorkdir,
|
|
639
|
+
errorCode
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
thread = await provider.startThread(threadStartSettings);
|
|
643
|
+
fresh = true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const threadId = codexAppServerThreadResponseId(thread, resumableThreadId);
|
|
647
|
+
if (
|
|
648
|
+
!threadId ||
|
|
649
|
+
threadId === normalizedForbiddenThreadId ||
|
|
650
|
+
(fresh && baselineThreadIds.includes(threadId))
|
|
651
|
+
) {
|
|
652
|
+
throw codexRenewalThreadError(
|
|
653
|
+
errorCode,
|
|
654
|
+
threadId
|
|
655
|
+
? "The assistant provider reused the old session's thread instead of starting a fresh one."
|
|
656
|
+
: "The assistant provider did not return a fresh renewal thread id.",
|
|
657
|
+
{
|
|
658
|
+
forbiddenThreadId: normalizedForbiddenThreadId,
|
|
659
|
+
threadId
|
|
660
|
+
}
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir });
|
|
664
|
+
if (fresh) {
|
|
665
|
+
try {
|
|
666
|
+
threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
|
|
667
|
+
provider,
|
|
668
|
+
threadId,
|
|
669
|
+
workdir: normalizedWorkdir,
|
|
670
|
+
errorCode
|
|
671
|
+
});
|
|
672
|
+
} catch (error) {
|
|
673
|
+
throw codexRenewalThreadError(
|
|
674
|
+
errorCode,
|
|
675
|
+
`The newly started renewal thread cannot be read; ${applicationName} will not replace it silently.`,
|
|
676
|
+
{
|
|
677
|
+
providerError: normalizeAgentText(error?.message),
|
|
678
|
+
threadId
|
|
679
|
+
},
|
|
680
|
+
{ retryable: true }
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return Object.freeze({
|
|
685
|
+
appServerRuntime,
|
|
686
|
+
fresh,
|
|
687
|
+
thread,
|
|
688
|
+
threadId,
|
|
689
|
+
threadSnapshot,
|
|
690
|
+
threadStartSettings,
|
|
691
|
+
threadSettings
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function codexAppServerModelCatalogError(message = "", errorPrefix = "") {
|
|
696
|
+
const error = new Error(normalizeAgentText(message) || "Codex returned an invalid model catalog.");
|
|
697
|
+
error.code = `${errorPrefix}codex_model_catalog_invalid`;
|
|
698
|
+
return error;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function codexAppServerProviderConnectionGeneration(provider = null) {
|
|
702
|
+
const generation = typeof provider?.currentConnectionGeneration === "function"
|
|
703
|
+
? provider.currentConnectionGeneration()
|
|
704
|
+
: typeof provider?.connectionGeneration === "function"
|
|
705
|
+
? provider.connectionGeneration()
|
|
706
|
+
: provider?.connectionGeneration;
|
|
707
|
+
return normalizeAgentText(generation || "");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function codexAppServerModelCatalogSnapshot(value = null, errorPrefix = "") {
|
|
711
|
+
if (!Array.isArray(value?.data)) {
|
|
712
|
+
throw codexAppServerModelCatalogError("Codex did not return a usable live model catalog.", errorPrefix);
|
|
713
|
+
}
|
|
714
|
+
return Object.freeze({
|
|
715
|
+
data: Object.freeze(value.data.map((model = {}) => Object.freeze({
|
|
716
|
+
hidden: model.hidden === true,
|
|
717
|
+
model: normalizeAgentText(model.model || ""),
|
|
718
|
+
supportedReasoningEfforts: Object.freeze((Array.isArray(model.supportedReasoningEfforts)
|
|
719
|
+
? model.supportedReasoningEfforts
|
|
720
|
+
: []).map((option = {}) => Object.freeze({
|
|
721
|
+
reasoningEffort: normalizeAgentText(option.reasoningEffort || "")
|
|
722
|
+
})))
|
|
723
|
+
})))
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function createCodexAppServerModelCatalogCache({
|
|
728
|
+
cacheMs = 30_000,
|
|
729
|
+
errorPrefix = ""
|
|
730
|
+
} = {}) {
|
|
731
|
+
const codexAppServerModelCatalogs = new WeakMap();
|
|
732
|
+
return async function readCodexAppServerModelCatalog(provider, { signal = null } = {}) {
|
|
733
|
+
if (typeof provider.listModels !== "function") {
|
|
734
|
+
const error = new Error("Codex live model discovery is unavailable.");
|
|
735
|
+
error.code = `${errorPrefix}codex_model_catalog_unavailable`;
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
const connectionGeneration = codexAppServerProviderConnectionGeneration(provider);
|
|
739
|
+
const now = Date.now();
|
|
740
|
+
const cached = codexAppServerModelCatalogs.get(provider);
|
|
741
|
+
if (
|
|
742
|
+
cached?.connectionGeneration === connectionGeneration &&
|
|
743
|
+
cached.value &&
|
|
744
|
+
cached.expiresAt > now
|
|
745
|
+
) {
|
|
746
|
+
return cached.value;
|
|
747
|
+
}
|
|
748
|
+
if (
|
|
749
|
+
cached?.connectionGeneration === connectionGeneration &&
|
|
750
|
+
cached.pending
|
|
751
|
+
) {
|
|
752
|
+
return cached.pending;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const pending = provider.listModels({
|
|
756
|
+
includeHidden: false,
|
|
757
|
+
limit: 100
|
|
758
|
+
}, { signal }).then((result) => {
|
|
759
|
+
if (codexAppServerProviderConnectionGeneration(provider) !== connectionGeneration) {
|
|
760
|
+
const error = new Error("Codex reconnected while resolving the helper model catalog.");
|
|
761
|
+
error.code = `${errorPrefix}codex_model_catalog_stale`;
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
return codexAppServerModelCatalogSnapshot(result, errorPrefix);
|
|
765
|
+
});
|
|
766
|
+
codexAppServerModelCatalogs.set(provider, {
|
|
767
|
+
connectionGeneration,
|
|
768
|
+
expiresAt: 0,
|
|
769
|
+
pending,
|
|
770
|
+
value: null
|
|
771
|
+
});
|
|
772
|
+
try {
|
|
773
|
+
const value = await pending;
|
|
774
|
+
codexAppServerModelCatalogs.set(provider, {
|
|
775
|
+
connectionGeneration,
|
|
776
|
+
expiresAt: Date.now() + cacheMs,
|
|
777
|
+
pending: null,
|
|
778
|
+
value
|
|
779
|
+
});
|
|
780
|
+
return value;
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if (codexAppServerModelCatalogs.get(provider)?.pending === pending) {
|
|
783
|
+
codexAppServerModelCatalogs.delete(provider);
|
|
784
|
+
}
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function codexAppServerHelperLoginParams(auth = {}, errorPrefix = "") {
|
|
791
|
+
if (auth.authMode === "chatgpt") {
|
|
792
|
+
return {
|
|
793
|
+
accessToken: auth.accessToken,
|
|
794
|
+
chatgptAccountId: auth.accountId,
|
|
795
|
+
chatgptPlanType: auth.planType || null,
|
|
796
|
+
type: "chatgptAuthTokens"
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (auth.authMode === "apiKey") {
|
|
800
|
+
return {
|
|
801
|
+
apiKey: auth.apiKey,
|
|
802
|
+
type: "apiKey"
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
throw codexAppServerHelperAuthError(
|
|
806
|
+
`${errorPrefix}codex_helper_auth_invalid`,
|
|
807
|
+
"The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function codexAppServerHelperLoginResponseType(auth = {}) {
|
|
812
|
+
return auth.authMode === "chatgpt" ? "chatgptAuthTokens" : "apiKey";
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function codexAppServerHelperAccountType(auth = {}) {
|
|
816
|
+
return auth.authMode === "chatgpt" ? "chatgpt" : "apiKey";
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function codexAppServerHelperRequestError(error = null, reason = "", errorPrefix = "") {
|
|
820
|
+
if (
|
|
821
|
+
error?.name === "AbortError" ||
|
|
822
|
+
error?.code === "ABORT_ERR" ||
|
|
823
|
+
(typeof error?.code === "string" && error.code.startsWith(`${errorPrefix}codex_helper_`)) ||
|
|
824
|
+
error?.code === `${errorPrefix}codex_model_catalog_invalid`
|
|
825
|
+
) {
|
|
826
|
+
return error;
|
|
827
|
+
}
|
|
828
|
+
const failure = new Error("Codex isolated helper execution failed. Retry the task.");
|
|
829
|
+
failure.code = `${errorPrefix}codex_helper_provider_request_failed`;
|
|
830
|
+
const providerCode = Number(error?.code);
|
|
831
|
+
if (Number.isSafeInteger(providerCode)) {
|
|
832
|
+
failure.providerCode = providerCode;
|
|
833
|
+
}
|
|
834
|
+
const normalizedReason = normalizeAgentText(reason);
|
|
835
|
+
if (normalizedReason && normalizedReason.length <= 128) {
|
|
836
|
+
failure.operation = normalizedReason;
|
|
837
|
+
}
|
|
838
|
+
return failure;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function normalizeCodexAppServerInfo(initializeResult = null) {
|
|
842
|
+
if (!isPlainObject(initializeResult)) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
const rawUserAgent = initializeResult.userAgent;
|
|
846
|
+
if (
|
|
847
|
+
typeof rawUserAgent !== "string" ||
|
|
848
|
+
rawUserAgent.length === 0 ||
|
|
849
|
+
rawUserAgent.length > CODEX_APP_SERVER_SERVER_INFO_USER_AGENT_MAX_LENGTH ||
|
|
850
|
+
codexAppServerTextHasControlCharacters(rawUserAgent)
|
|
851
|
+
) {
|
|
852
|
+
return Object.freeze({ userAgent: "" });
|
|
853
|
+
}
|
|
854
|
+
return Object.freeze({
|
|
855
|
+
userAgent: rawUserAgent.trim()
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function tailAppend(text = "", chunk = "", maxBytes = CODEX_AUTH_PREFLIGHT_OUTPUT_TAIL_BYTES) {
|
|
860
|
+
const next = `${String(text || "")}${String(chunk || "")}`;
|
|
861
|
+
return next.length > maxBytes ? next.slice(-maxBytes) : next;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function codexAuthPreflightArgs() {
|
|
865
|
+
return [
|
|
866
|
+
"-c",
|
|
867
|
+
"check_for_update_on_startup=false",
|
|
868
|
+
"debug",
|
|
869
|
+
"models"
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function runCodexAuthPreflight({
|
|
874
|
+
codexCommand = "codex",
|
|
875
|
+
env = process.env,
|
|
876
|
+
executionRoot = "",
|
|
877
|
+
runtimeDir = "",
|
|
878
|
+
terminalEnv = {},
|
|
879
|
+
timeoutMs = CODEX_AUTH_PREFLIGHT_TIMEOUT_MS,
|
|
880
|
+
toolHomeSource = "",
|
|
881
|
+
workdir = ""
|
|
882
|
+
} = {}, execution) {
|
|
883
|
+
const normalizedToolHomeSource = normalizeAgentText(toolHomeSource);
|
|
884
|
+
if (normalizedToolHomeSource) {
|
|
885
|
+
await assertExistingDirectory(normalizedToolHomeSource, "Codex credential home");
|
|
886
|
+
}
|
|
887
|
+
const normalizedRuntimeDir = normalizeAgentText(runtimeDir);
|
|
888
|
+
if (normalizedRuntimeDir) {
|
|
889
|
+
await ensureWritablePrivateDirectory(path.resolve(normalizedRuntimeDir));
|
|
890
|
+
}
|
|
891
|
+
const baseEnv = codexAppServerCommandBaseEnv({
|
|
892
|
+
env,
|
|
893
|
+
terminalEnv
|
|
894
|
+
});
|
|
895
|
+
const processCwd = codexAppServerProcessCwd({
|
|
896
|
+
executionRoot,
|
|
897
|
+
runtimeDir,
|
|
898
|
+
workdir
|
|
899
|
+
});
|
|
900
|
+
try {
|
|
901
|
+
const result = await execution.run({
|
|
902
|
+
args: codexAuthPreflightArgs(),
|
|
903
|
+
baseEnv,
|
|
904
|
+
command: codexCommand,
|
|
905
|
+
credentialHome: codexAppServerCredentialHome(normalizedToolHomeSource, baseEnv),
|
|
906
|
+
cwd: processCwd,
|
|
907
|
+
inheritProcessEnv: false,
|
|
908
|
+
mode: "capture",
|
|
909
|
+
timeout: normalizePositiveInteger(timeoutMs, CODEX_AUTH_PREFLIGHT_TIMEOUT_MS)
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
code: result.exitCode,
|
|
913
|
+
ok: result.ok === true,
|
|
914
|
+
output: tailAppend("", result.output || [
|
|
915
|
+
result.stderr,
|
|
916
|
+
result.stdout
|
|
917
|
+
].filter(Boolean).join("\n")),
|
|
918
|
+
signal: result.signal,
|
|
919
|
+
timedOut: result.timedOut === true
|
|
920
|
+
};
|
|
921
|
+
} catch (error) {
|
|
922
|
+
return {
|
|
923
|
+
error,
|
|
924
|
+
ok: false,
|
|
925
|
+
output: normalizeAgentText(error?.message || error)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function codexAppServerEndpointForTarget(endpoint = "") {
|
|
931
|
+
const normalizedEndpoint = normalizeAgentText(endpoint);
|
|
932
|
+
if (!normalizedEndpoint) {
|
|
933
|
+
return "";
|
|
934
|
+
}
|
|
935
|
+
return normalizedEndpoint;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function codexAppServerThreadRequestParams(params = {}, threadEnv = {}) {
|
|
939
|
+
const source = isPlainObject(params) ? params : {};
|
|
940
|
+
const config = isPlainObject(source.config) ? source.config : {};
|
|
941
|
+
if (Object.hasOwn(config, "shell_environment_policy")) {
|
|
942
|
+
return source;
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
...source,
|
|
946
|
+
config: {
|
|
947
|
+
...config,
|
|
948
|
+
shell_environment_policy: {
|
|
949
|
+
inherit: "none",
|
|
950
|
+
set: normalizeCodexAppServerTerminalEnv(threadEnv)
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function codexTextInput(text = "") {
|
|
957
|
+
return {
|
|
958
|
+
text: String(text ?? ""),
|
|
959
|
+
text_elements: [],
|
|
960
|
+
type: "text"
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function codexLocalImageInput(attachments = []) {
|
|
965
|
+
return attachments
|
|
966
|
+
.filter((attachment) => attachment.contentType?.startsWith("image/"))
|
|
967
|
+
.map((attachment) => ({ type: "localImage", path: attachment.path }));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function sendCodexAppServerPrompt({
|
|
971
|
+
attachments = [],
|
|
972
|
+
clientUserMessageId = "",
|
|
973
|
+
outputSchema = null,
|
|
974
|
+
provider,
|
|
975
|
+
prompt = "",
|
|
976
|
+
threadId = "",
|
|
977
|
+
readOnly = false
|
|
978
|
+
} = {}, authorized = {}) {
|
|
979
|
+
const authoredInput = String(prompt ?? "");
|
|
980
|
+
const preparedInput = authorized.preparedInput;
|
|
981
|
+
if (preparedInput === undefined && !authoredInput.trim()) {
|
|
982
|
+
throw new Error("Codex app-server prompt is empty.");
|
|
983
|
+
}
|
|
984
|
+
const input = preparedInput === undefined ? [authoredInput, ...codexLocalImageInput(attachments)] : preparedInput;
|
|
985
|
+
const turnSettings = {
|
|
986
|
+
...authorized.turnSettings,
|
|
987
|
+
...(readOnly
|
|
988
|
+
? {
|
|
989
|
+
sandboxPolicy: {
|
|
990
|
+
networkAccess: false,
|
|
991
|
+
type: "readOnly"
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
: {}),
|
|
995
|
+
...(normalizeAgentText(clientUserMessageId)
|
|
996
|
+
? { clientUserMessageId: normalizeAgentText(clientUserMessageId) }
|
|
997
|
+
: {})
|
|
998
|
+
};
|
|
999
|
+
if (outputSchema && typeof outputSchema === "object" && !Array.isArray(outputSchema)) {
|
|
1000
|
+
turnSettings.outputSchema = outputSchema;
|
|
1001
|
+
}
|
|
1002
|
+
const turn = await provider.sendTurn(threadId, input, turnSettings);
|
|
1003
|
+
return {
|
|
1004
|
+
input,
|
|
1005
|
+
turn
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function codexTurnInput(input = []) {
|
|
1010
|
+
const values = Array.isArray(input) ? input : [input];
|
|
1011
|
+
return values.map((item) => {
|
|
1012
|
+
if (isPlainObject(item) && item.type === "localImage" && typeof item.path === "string") {
|
|
1013
|
+
return { type: "localImage", path: item.path };
|
|
1014
|
+
}
|
|
1015
|
+
if (isPlainObject(item) && item.type === "image" && typeof item.url === "string") {
|
|
1016
|
+
return { type: "image", url: item.url };
|
|
1017
|
+
}
|
|
1018
|
+
if (isPlainObject(item) && item.type === "text") {
|
|
1019
|
+
return codexTextInput(item.text);
|
|
1020
|
+
}
|
|
1021
|
+
return codexTextInput(item);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function shellQuote(value = "") {
|
|
1026
|
+
const text = String(value ?? "");
|
|
1027
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/u.test(text)) {
|
|
1028
|
+
return text;
|
|
1029
|
+
}
|
|
1030
|
+
return `'${text.replaceAll("'", "'\"'\"'")}'`;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function codexControlProbeCommand(keys, marker) {
|
|
1034
|
+
const source = [
|
|
1035
|
+
"const {createHash}=require('node:crypto');",
|
|
1036
|
+
`const keys=${JSON.stringify(keys)};`,
|
|
1037
|
+
`process.stdout.write(${JSON.stringify(marker)}+createHash('sha256').update(JSON.stringify(keys.map(key=>[key,process.env[key]]))).digest('hex'))`
|
|
1038
|
+
].join("");
|
|
1039
|
+
return `${shellQuote(process.execPath)} -e ${shellQuote(source)}`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function codexCliResumeCommand({
|
|
1043
|
+
codexCommand = "",
|
|
1044
|
+
endpoint = "",
|
|
1045
|
+
threadId = ""
|
|
1046
|
+
} = {}) {
|
|
1047
|
+
const normalizedEndpoint = codexAppServerEndpointForTarget(endpoint);
|
|
1048
|
+
const resolvedCodexCommand = normalizeAgentText(codexCommand) || "codex";
|
|
1049
|
+
const normalizedThreadId = normalizeAgentText(threadId);
|
|
1050
|
+
if (!normalizedEndpoint) {
|
|
1051
|
+
throw new Error("Codex app-server endpoint is required for the native CLI command.");
|
|
1052
|
+
}
|
|
1053
|
+
if (!normalizedThreadId) {
|
|
1054
|
+
throw new Error("Codex thread id is required for the native CLI command.");
|
|
1055
|
+
}
|
|
1056
|
+
const argv = [
|
|
1057
|
+
resolvedCodexCommand,
|
|
1058
|
+
"-c",
|
|
1059
|
+
"check_for_update_on_startup=false",
|
|
1060
|
+
"--remote",
|
|
1061
|
+
normalizedEndpoint,
|
|
1062
|
+
"resume",
|
|
1063
|
+
normalizedThreadId
|
|
1064
|
+
];
|
|
1065
|
+
return {
|
|
1066
|
+
argv,
|
|
1067
|
+
command: argv.map(shellQuote).join(" ")
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function codexAppServerThreadInventoryError(errorCode, label, detail) {
|
|
1072
|
+
const error = new Error(`Codex ${label} thread inventory ${detail}.`);
|
|
1073
|
+
error.code = errorCode;
|
|
1074
|
+
return error;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function listBoundedCodexAppServerThreadIds({
|
|
1078
|
+
ancestorThreadId = "",
|
|
1079
|
+
archived = false,
|
|
1080
|
+
client,
|
|
1081
|
+
cwd = "",
|
|
1082
|
+
errorCode = "",
|
|
1083
|
+
label = "",
|
|
1084
|
+
requestLabel = "",
|
|
1085
|
+
runRequest,
|
|
1086
|
+
signal = null,
|
|
1087
|
+
sourceKinds = ["appServer"],
|
|
1088
|
+
state,
|
|
1089
|
+
verifyCwd = false
|
|
1090
|
+
} = {}) {
|
|
1091
|
+
const seenCursors = new Set();
|
|
1092
|
+
let cursor = "";
|
|
1093
|
+
for (let page = 0; page < CODEX_APP_SERVER_THREAD_INVENTORY_MAX_PAGES; page += 1) {
|
|
1094
|
+
if (cursor && seenCursors.has(cursor)) {
|
|
1095
|
+
throw codexAppServerThreadInventoryError(
|
|
1096
|
+
errorCode,
|
|
1097
|
+
label,
|
|
1098
|
+
"repeated a pagination cursor"
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
if (cursor) {
|
|
1102
|
+
seenCursors.add(cursor);
|
|
1103
|
+
}
|
|
1104
|
+
const response = await runRequest(
|
|
1105
|
+
() => client.request("thread/list", {
|
|
1106
|
+
archived,
|
|
1107
|
+
...(cursor ? { cursor } : {}),
|
|
1108
|
+
...(ancestorThreadId ? { ancestorThreadId } : cwd ? { cwd } : {}),
|
|
1109
|
+
limit: CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT,
|
|
1110
|
+
modelProviders: [],
|
|
1111
|
+
sourceKinds,
|
|
1112
|
+
useStateDbOnly: false
|
|
1113
|
+
}, { signal }),
|
|
1114
|
+
requestLabel
|
|
1115
|
+
);
|
|
1116
|
+
if (
|
|
1117
|
+
!Array.isArray(response?.data) ||
|
|
1118
|
+
response.data.length > CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT ||
|
|
1119
|
+
state.entryCount + response.data.length > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT
|
|
1120
|
+
) {
|
|
1121
|
+
throw codexAppServerThreadInventoryError(
|
|
1122
|
+
errorCode,
|
|
1123
|
+
label,
|
|
1124
|
+
"exceeded its entry limit"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
for (const entry of response.data) {
|
|
1128
|
+
const threadId = normalizeAgentText(entry?.id);
|
|
1129
|
+
const threadCwd = normalizeAgentText(entry?.cwd);
|
|
1130
|
+
let entryBytes = 0;
|
|
1131
|
+
try {
|
|
1132
|
+
entryBytes = Buffer.byteLength(JSON.stringify(entry), "utf8");
|
|
1133
|
+
} catch {
|
|
1134
|
+
entryBytes = CODEX_APP_SERVER_THREAD_INVENTORY_MAX_ENTRY_BYTES + 1;
|
|
1135
|
+
}
|
|
1136
|
+
if (
|
|
1137
|
+
!isPlainObject(entry) ||
|
|
1138
|
+
!threadId ||
|
|
1139
|
+
threadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
|
|
1140
|
+
codexAppServerTextHasControlCharacters(threadId) ||
|
|
1141
|
+
(verifyCwd && threadCwd !== cwd) ||
|
|
1142
|
+
entryBytes > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_ENTRY_BYTES ||
|
|
1143
|
+
state.totalBytes + entryBytes > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_TOTAL_BYTES
|
|
1144
|
+
) {
|
|
1145
|
+
throw codexAppServerThreadInventoryError(
|
|
1146
|
+
errorCode,
|
|
1147
|
+
label,
|
|
1148
|
+
"contained an invalid entry"
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
state.entryCount += 1;
|
|
1152
|
+
state.totalBytes += entryBytes;
|
|
1153
|
+
state.threadIds.add(threadId);
|
|
1154
|
+
}
|
|
1155
|
+
const nextCursor = response.nextCursor === undefined || response.nextCursor === null || response.nextCursor === ""
|
|
1156
|
+
? ""
|
|
1157
|
+
: typeof response.nextCursor === "string"
|
|
1158
|
+
? response.nextCursor.trim()
|
|
1159
|
+
: null;
|
|
1160
|
+
if (
|
|
1161
|
+
nextCursor === null ||
|
|
1162
|
+
nextCursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH ||
|
|
1163
|
+
(nextCursor && seenCursors.has(nextCursor))
|
|
1164
|
+
) {
|
|
1165
|
+
throw codexAppServerThreadInventoryError(
|
|
1166
|
+
errorCode,
|
|
1167
|
+
label,
|
|
1168
|
+
"returned an invalid pagination cursor"
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
if (!nextCursor) {
|
|
1172
|
+
return state;
|
|
1173
|
+
}
|
|
1174
|
+
cursor = nextCursor;
|
|
1175
|
+
}
|
|
1176
|
+
throw codexAppServerThreadInventoryError(
|
|
1177
|
+
errorCode,
|
|
1178
|
+
label,
|
|
1179
|
+
"exceeded its page limit"
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function codexPlanUsage(limits = {}) {
|
|
1184
|
+
if (limits?.limitId && limits.limitId !== "codex") return { status: "unavailable", windows: [], checkedAt: Date.now() };
|
|
1185
|
+
const windows = ["primary", "secondary"].flatMap((id) => {
|
|
1186
|
+
const value = limits?.[id];
|
|
1187
|
+
if (typeof value?.usedPercent !== "number" || !Number.isFinite(value.usedPercent) || value.usedPercent < 0) return [];
|
|
1188
|
+
return [{
|
|
1189
|
+
id,
|
|
1190
|
+
remainingPercent: Math.max(0, 100 - Math.min(100, value.usedPercent)),
|
|
1191
|
+
windowDurationMins: Number.isInteger(value.windowDurationMins) && value.windowDurationMins > 0 ? value.windowDurationMins : null,
|
|
1192
|
+
resetsAt: Number.isSafeInteger(value.resetsAt) && value.resetsAt > 0 ? value.resetsAt : null
|
|
1193
|
+
}];
|
|
1194
|
+
});
|
|
1195
|
+
return { status: windows.length ? "available" : "unavailable", windows, checkedAt: Date.now() };
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function normalizeAgentThread(value = {}) {
|
|
1199
|
+
const thread = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1200
|
+
return {
|
|
1201
|
+
id: normalizeAgentText(thread.id || thread.threadId),
|
|
1202
|
+
provider: normalizeAgentText(thread.provider),
|
|
1203
|
+
raw: thread.raw || thread
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function normalizeAgentTurn(value = {}) {
|
|
1208
|
+
const turn = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1209
|
+
return {
|
|
1210
|
+
id: normalizeAgentText(turn.id || turn.turnId),
|
|
1211
|
+
provider: normalizeAgentText(turn.provider),
|
|
1212
|
+
raw: turn.raw || turn,
|
|
1213
|
+
status: normalizeAgentText(turn.status || turn.raw?.status)
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async function assertCodexAuthPreflightReady(options = {}, {
|
|
1218
|
+
reason = "codex-auth-preflight", execution, credentials
|
|
1219
|
+
} = {}) {
|
|
1220
|
+
const result = await runCodexAuthPreflight(options, execution);
|
|
1221
|
+
if (result.ok && !codexAuthOutputRequiresReconnect(result.output)) {
|
|
1222
|
+
return result;
|
|
1223
|
+
}
|
|
1224
|
+
const observed = normalizeAgentText(result.output || result.error?.message || "Codex auth preflight failed.");
|
|
1225
|
+
if (codexAuthOutputRequiresReconnect(observed)) {
|
|
1226
|
+
await credentials.markInvalid( {
|
|
1227
|
+
reason
|
|
1228
|
+
});
|
|
1229
|
+
throw credentials.reconnectError({
|
|
1230
|
+
modelProviderId: "",
|
|
1231
|
+
observed
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
throw new Error(observed || "Codex auth preflight failed.");
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export class CodexAppServerAgentProvider {
|
|
1238
|
+
constructor(options = {}, host = {}) {
|
|
1239
|
+
this.host = host;
|
|
1240
|
+
this.credentials = host.credentials;
|
|
1241
|
+
this.configuration = {
|
|
1242
|
+
errorPrefix: "",
|
|
1243
|
+
clientInfo: { name: "jskit", title: "JSKIT", version: "0.1.0" },
|
|
1244
|
+
historyClientInfo: { name: "jskit-history-export", title: "JSKIT", version: "0.1.0" },
|
|
1245
|
+
controlEnvironmentPrefix: "JSKIT_",
|
|
1246
|
+
controlProbePrefix: "JSKIT_CONTROL_CHECK_",
|
|
1247
|
+
...host.configuration
|
|
1248
|
+
};
|
|
1249
|
+
this.errorPrefix = this.configuration.errorPrefix;
|
|
1250
|
+
this.accountReader = createCodexAccountReader({ errorPrefix: this.errorPrefix, ...this.configuration.accountReader });
|
|
1251
|
+
this.modelCatalogError = (message) => codexAppServerModelCatalogError(message, this.errorPrefix);
|
|
1252
|
+
this.planUsage = null;
|
|
1253
|
+
this.planUsagePending = null;
|
|
1254
|
+
this.planUsageAuthGeneration = 0;
|
|
1255
|
+
this.availabilityPromise = null;
|
|
1256
|
+
this.options = options;
|
|
1257
|
+
this.client = null;
|
|
1258
|
+
this.connectPromise = null;
|
|
1259
|
+
this.connectionGeneration = 0;
|
|
1260
|
+
this.notificationSubscribers = new Set();
|
|
1261
|
+
this.helperAuth = null;
|
|
1262
|
+
this.helperAuthBlocked = false;
|
|
1263
|
+
this.initializeResult = null;
|
|
1264
|
+
this.runtime = null;
|
|
1265
|
+
this.runtimeStopOwner = null;
|
|
1266
|
+
this.runtimePromise = null;
|
|
1267
|
+
this.serverRequestHandler = null;
|
|
1268
|
+
this.conversationRuntime = createCodexConversationAdapter({
|
|
1269
|
+
readInstructions: (params, threadId) => this.options.readInstructions?.(params, threadId),
|
|
1270
|
+
client: () => this.activeClient(),
|
|
1271
|
+
runtime: () => this.runtime,
|
|
1272
|
+
runRequest: (run, reason) => this.runRequest(run, reason),
|
|
1273
|
+
timeoutMs: options.threadControlTimeoutMs,
|
|
1274
|
+
environment: options.threadEnv,
|
|
1275
|
+
prepareEnvironment: (environment) => this.options.prepareThreadEnvironment(environment),
|
|
1276
|
+
prepareParameters: (params) => this.options.prepareThreadParams?.(params),
|
|
1277
|
+
prepareResume: (id, params, context) => this.options.prepareThreadResumeParams?.(id, params, context),
|
|
1278
|
+
beforeResume: (id) => this.options.beforeResumeThread?.(id),
|
|
1279
|
+
prepareHistory: (params, client, context) => this.withHistoryAdapter(params, client, context),
|
|
1280
|
+
threadParameters: codexAppServerThreadRequestParams,
|
|
1281
|
+
verifyEnvironment: (...args) => this.#verifyThreadEnvironment(...args),
|
|
1282
|
+
onRecoveryFailure: (id) => {
|
|
1283
|
+
const probe = this.threadControlProbes.get(id);
|
|
1284
|
+
if (probe && !probe.verified) {
|
|
1285
|
+
this.threadControlProbes.delete(id);
|
|
1286
|
+
for (const pending of probe.pending) this.publishNotification(pending);
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
log: (event, fields, context) => host.log?.(event, fields, {
|
|
1290
|
+
...context, connectionGeneration: this.connectionGeneration
|
|
1291
|
+
})
|
|
1292
|
+
});
|
|
1293
|
+
this.threadControlProbes = new Map();
|
|
1294
|
+
this.commandExecutions = new Map();
|
|
1295
|
+
this.interruptedTurns = new Set();
|
|
1296
|
+
this.commandStopFailure = null;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
get threadEnvironments() { return this.conversationRuntime.threadEnvironments; }
|
|
1300
|
+
|
|
1301
|
+
get threadEnvironmentTasks() { return this.conversationRuntime.threadEnvironmentTasks; }
|
|
1302
|
+
|
|
1303
|
+
isHelperProvider() {
|
|
1304
|
+
return codexAppServerIsHelper(this.options);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async selectedHelperAuth() {
|
|
1308
|
+
if (!this.isHelperProvider()) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
if (this.host.parameters.providerHome) {
|
|
1312
|
+
return { identitySignature: await this.credentials.identity(this.host.parameters) };
|
|
1313
|
+
}
|
|
1314
|
+
return this.accountReader.readSelected(this.options);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async assertHelperAccountIdentityCurrent() {
|
|
1318
|
+
if (!this.isHelperProvider() || !this.helperAuth) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (this.helperAuthBlocked) {
|
|
1322
|
+
throw this.accountReader.error(
|
|
1323
|
+
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
1324
|
+
"The selected Codex account changed during helper work. Retry the task with the current account."
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
const current = await this.selectedHelperAuth();
|
|
1328
|
+
if (current.identitySignature !== this.helperAuth.identitySignature) {
|
|
1329
|
+
this.helperAuthBlocked = true;
|
|
1330
|
+
throw this.accountReader.error(
|
|
1331
|
+
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
1332
|
+
"The selected Codex account changed during helper work. Retry the task with the current account."
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async authenticateHelperClient(client, runtime = {}) {
|
|
1338
|
+
if (!this.isHelperProvider()) {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
const auth = await this.selectedHelperAuth();
|
|
1342
|
+
if (
|
|
1343
|
+
!runtime.accountIdentitySignature ||
|
|
1344
|
+
auth.identitySignature !== runtime.accountIdentitySignature
|
|
1345
|
+
) {
|
|
1346
|
+
throw this.accountReader.error(
|
|
1347
|
+
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
1348
|
+
"The selected Codex account changed while helper execution was starting. Retry the task."
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
this.helperAuth = auth;
|
|
1352
|
+
this.helperAuthBlocked = false;
|
|
1353
|
+
if (this.host.parameters.providerHome) return auth;
|
|
1354
|
+
try {
|
|
1355
|
+
const login = await client.request(
|
|
1356
|
+
"account/login/start",
|
|
1357
|
+
codexAppServerHelperLoginParams(auth, this.errorPrefix)
|
|
1358
|
+
);
|
|
1359
|
+
if (normalizeAgentText(login?.type) !== codexAppServerHelperLoginResponseType(auth)) {
|
|
1360
|
+
throw new Error("invalid login response");
|
|
1361
|
+
}
|
|
1362
|
+
const account = await client.request("account/read", {
|
|
1363
|
+
refreshToken: false
|
|
1364
|
+
});
|
|
1365
|
+
if (
|
|
1366
|
+
account?.requiresOpenaiAuth !== true ||
|
|
1367
|
+
normalizeAgentText(account?.account?.type) !== codexAppServerHelperAccountType(auth)
|
|
1368
|
+
) {
|
|
1369
|
+
throw new Error("invalid account response");
|
|
1370
|
+
}
|
|
1371
|
+
return auth;
|
|
1372
|
+
} catch {
|
|
1373
|
+
this.helperAuth = null;
|
|
1374
|
+
this.helperAuthBlocked = true;
|
|
1375
|
+
throw this.accountReader.error(
|
|
1376
|
+
`${this.errorPrefix}codex_helper_auth_unavailable`,
|
|
1377
|
+
"Codex could not activate the selected account for isolated helper work. Reconnect Codex and retry."
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async refreshHelperChatgptAuth(params = {}) {
|
|
1383
|
+
const currentAuth = this.helperAuth;
|
|
1384
|
+
const previousAccountId = params.previousAccountId;
|
|
1385
|
+
if (
|
|
1386
|
+
!this.isHelperProvider() ||
|
|
1387
|
+
currentAuth?.authMode !== "chatgpt" ||
|
|
1388
|
+
params.reason !== "unauthorized" ||
|
|
1389
|
+
typeof previousAccountId !== "string" ||
|
|
1390
|
+
previousAccountId.length === 0 ||
|
|
1391
|
+
previousAccountId.length > CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH ||
|
|
1392
|
+
previousAccountId !== currentAuth.accountId
|
|
1393
|
+
) {
|
|
1394
|
+
this.helperAuthBlocked = true;
|
|
1395
|
+
throw this.accountReader.error(
|
|
1396
|
+
`${this.errorPrefix}codex_helper_auth_refresh_rejected`,
|
|
1397
|
+
"Codex helper authentication refresh was rejected. Reconnect Codex and retry."
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
const refreshed = await this.selectedHelperAuth();
|
|
1401
|
+
if (
|
|
1402
|
+
refreshed.authMode !== "chatgpt" ||
|
|
1403
|
+
refreshed.identitySignature !== currentAuth.identitySignature
|
|
1404
|
+
) {
|
|
1405
|
+
this.helperAuthBlocked = true;
|
|
1406
|
+
throw this.accountReader.error(
|
|
1407
|
+
`${this.errorPrefix}codex_helper_auth_changed`,
|
|
1408
|
+
"The selected Codex account changed during helper work. Retry the task with the current account."
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (refreshed.secretSignature === currentAuth.secretSignature) {
|
|
1412
|
+
this.helperAuthBlocked = true;
|
|
1413
|
+
throw this.accountReader.error(
|
|
1414
|
+
`${this.errorPrefix}codex_helper_auth_refresh_pending`,
|
|
1415
|
+
"The selected Codex account has not produced a newer access token yet. Reconnect Codex and retry."
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
this.helperAuth = refreshed;
|
|
1419
|
+
return {
|
|
1420
|
+
accessToken: refreshed.accessToken,
|
|
1421
|
+
chatgptAccountId: refreshed.accountId,
|
|
1422
|
+
chatgptPlanType: refreshed.planType || null
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
async handleServerRequest(request = {}) {
|
|
1427
|
+
if (
|
|
1428
|
+
this.isHelperProvider() &&
|
|
1429
|
+
request.method === CODEX_APP_SERVER_CHATGPT_REFRESH_METHOD
|
|
1430
|
+
) {
|
|
1431
|
+
return this.refreshHelperChatgptAuth(request.params);
|
|
1432
|
+
}
|
|
1433
|
+
if (this.isHelperProvider()) {
|
|
1434
|
+
const error = new Error("Codex isolated helper execution does not accept server requests.");
|
|
1435
|
+
error.code = -32601;
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1438
|
+
if (typeof this.serverRequestHandler === "function") {
|
|
1439
|
+
return this.serverRequestHandler(request);
|
|
1440
|
+
}
|
|
1441
|
+
const error = new Error("Codex app-server request is not supported by this client.");
|
|
1442
|
+
error.code = -32601;
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async ensureRuntime() {
|
|
1447
|
+
if (this.runtimePromise) {
|
|
1448
|
+
return this.runtimePromise;
|
|
1449
|
+
}
|
|
1450
|
+
const operation = this.prepareRuntime();
|
|
1451
|
+
this.runtimePromise = operation;
|
|
1452
|
+
try {
|
|
1453
|
+
return await operation;
|
|
1454
|
+
} finally {
|
|
1455
|
+
if (this.runtimePromise === operation) {
|
|
1456
|
+
this.runtimePromise = null;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
async prepareRuntime() {
|
|
1462
|
+
const previousRuntime = this.runtime;
|
|
1463
|
+
const nextRuntime = await this.host.runtime.ensure(this.host.parameters);
|
|
1464
|
+
if (this.options.modelProviderId) nextRuntime.modelProviderId = this.options.modelProviderId;
|
|
1465
|
+
if (
|
|
1466
|
+
this.client &&
|
|
1467
|
+
previousRuntime &&
|
|
1468
|
+
codexAppServerRuntimeIdentity(previousRuntime) !== codexAppServerRuntimeIdentity(nextRuntime)
|
|
1469
|
+
) {
|
|
1470
|
+
this.client.close();
|
|
1471
|
+
this.client = null;
|
|
1472
|
+
this.initializeResult = null;
|
|
1473
|
+
}
|
|
1474
|
+
this.runtime = nextRuntime;
|
|
1475
|
+
this.runtimeStopOwner = nextRuntime;
|
|
1476
|
+
await this.assertRuntimeAuthReady("codex-app-server-runtime");
|
|
1477
|
+
return this.runtime;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
async preflightAuth(reason = "codex-auth-preflight", modelProviderId = "") {
|
|
1481
|
+
if (await this.options.prepareAuth?.(modelProviderId) === "external") {
|
|
1482
|
+
await this.assertRuntimeAuthReady(reason);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.options.modelProviderId && this.options.modelProviderId !== "openai") {
|
|
1486
|
+
await this.credentials.assertCurrent("");
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
await this.assertRuntimeAuthReady(reason);
|
|
1490
|
+
try {
|
|
1491
|
+
return await assertCodexAuthPreflightReady({ ...this.options, codexCommand: this.host.parameters.codexCommand }, {
|
|
1492
|
+
execution: this.host.execution, credentials: this.credentials, reason
|
|
1493
|
+
});
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
if (error?.code === this.credentials.reconnectRequiredCode && this.runtime?.runtimeDir) {
|
|
1496
|
+
await this.stopRuntimeAndRequireDrain(reason);
|
|
1497
|
+
}
|
|
1498
|
+
throw error;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async assertRuntimeAuthReady(reason = "codex-app-server") {
|
|
1503
|
+
if (this.isHelperProvider()) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const runtime = this.runtime || {};
|
|
1507
|
+
let generationError = null;
|
|
1508
|
+
try {
|
|
1509
|
+
await this.credentials.assertCurrent(runtime.authStateSignature);
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
generationError = error;
|
|
1512
|
+
}
|
|
1513
|
+
const logTail = await tailTextFile(runtime.logPath || "");
|
|
1514
|
+
const reconnectRequired = codexAuthOutputRequiresReconnect(logTail);
|
|
1515
|
+
if (!generationError && !reconnectRequired) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
await this.stopRuntimeAndRequireDrain(reason);
|
|
1519
|
+
if (reconnectRequired) {
|
|
1520
|
+
await this.markReconnectRequired({
|
|
1521
|
+
toolHomeSource: runtime.toolHomeSource || this.options.toolHomeSource,
|
|
1522
|
+
observed: logTail,
|
|
1523
|
+
reason
|
|
1524
|
+
});
|
|
1525
|
+
throw this.credentials.reconnectError({
|
|
1526
|
+
observed: logTail
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
throw generationError;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
async markReconnectRequired({ reason = "codex-app-server", observed = "", toolHomeSource } = {}) {
|
|
1533
|
+
await this.credentials.markInvalid({ reason, toolHomeSource });
|
|
1534
|
+
throw this.credentials.reconnectError({ modelProviderId: this.options.modelProviderId, observed });
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async runRequest(operation, reason = "codex-app-server-request") {
|
|
1538
|
+
try {
|
|
1539
|
+
await this.assertHelperAccountIdentityCurrent();
|
|
1540
|
+
const result = await operation();
|
|
1541
|
+
await this.assertHelperAccountIdentityCurrent();
|
|
1542
|
+
await this.assertRuntimeAuthReady(reason);
|
|
1543
|
+
return result;
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
if (this.isHelperProvider()) {
|
|
1546
|
+
throw codexAppServerHelperRequestError(error, reason, this.errorPrefix);
|
|
1547
|
+
}
|
|
1548
|
+
const observed = [
|
|
1549
|
+
error?.message || "",
|
|
1550
|
+
error?.observed || "",
|
|
1551
|
+
await tailTextFile(this.runtime?.logPath || "")
|
|
1552
|
+
].filter(Boolean).join("\n");
|
|
1553
|
+
if (codexAuthOutputRequiresReconnect(observed)) {
|
|
1554
|
+
const runtime = this.runtime || {};
|
|
1555
|
+
if (runtime.runtimeDir) {
|
|
1556
|
+
await this.stopRuntimeAndRequireDrain(reason);
|
|
1557
|
+
}
|
|
1558
|
+
await this.markReconnectRequired({
|
|
1559
|
+
toolHomeSource: runtime.toolHomeSource || this.options.toolHomeSource,
|
|
1560
|
+
observed,
|
|
1561
|
+
reason
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
throw error;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
async stopRuntimeAndRequireDrain(reason = "codex-app-server-stop") {
|
|
1569
|
+
const result = await this.stopRuntime({
|
|
1570
|
+
preserveProcessExitProof: !this.isHelperProvider()
|
|
1571
|
+
});
|
|
1572
|
+
if (
|
|
1573
|
+
result?.processExitVerified === true ||
|
|
1574
|
+
result?.runtimeDirRemoved === true ||
|
|
1575
|
+
result?.stopped === true
|
|
1576
|
+
) {
|
|
1577
|
+
return result;
|
|
1578
|
+
}
|
|
1579
|
+
const error = new Error("Codex app-server execution could not be proven empty.");
|
|
1580
|
+
error.code = this.isHelperProvider()
|
|
1581
|
+
? `${this.errorPrefix}codex_helper_runtime_cleanup_required`
|
|
1582
|
+
: `${this.errorPrefix}codex_app_server_cleanup_required`;
|
|
1583
|
+
error.cleanupRequired = true;
|
|
1584
|
+
error.reason = normalizeAgentText(reason);
|
|
1585
|
+
error.retryable = false;
|
|
1586
|
+
throw error;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
async connect() {
|
|
1590
|
+
if (this.observationFailure) throw this.observationFailure;
|
|
1591
|
+
if (this.client?.isOpen?.() && this.runtime) {
|
|
1592
|
+
return {
|
|
1593
|
+
initializeResult: null,
|
|
1594
|
+
reusedClient: true,
|
|
1595
|
+
runtime: this.runtime
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
if (this.connectPromise) {
|
|
1599
|
+
return this.connectPromise;
|
|
1600
|
+
}
|
|
1601
|
+
const operation = this.openConnection();
|
|
1602
|
+
this.connectPromise = operation;
|
|
1603
|
+
try {
|
|
1604
|
+
return await operation;
|
|
1605
|
+
} finally {
|
|
1606
|
+
if (this.connectPromise === operation) {
|
|
1607
|
+
this.connectPromise = null;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
async openConnection() {
|
|
1613
|
+
if (this.client?.isOpen?.() && this.runtime) {
|
|
1614
|
+
return {
|
|
1615
|
+
initializeResult: null,
|
|
1616
|
+
reusedClient: true,
|
|
1617
|
+
runtime: this.runtime
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
const runtime = await this.ensureRuntime();
|
|
1621
|
+
if (this.client?.isOpen?.()) {
|
|
1622
|
+
return {
|
|
1623
|
+
initializeResult: null,
|
|
1624
|
+
reusedClient: true,
|
|
1625
|
+
runtime
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
this.client?.close?.();
|
|
1629
|
+
this.client = null;
|
|
1630
|
+
this.initializeResult = null;
|
|
1631
|
+
const client = new CodexAppServerJsonRpcClient({
|
|
1632
|
+
endpoint: runtime.endpoint,
|
|
1633
|
+
onDisconnect: (error) => {
|
|
1634
|
+
if (this.client === client) this.failObservation(error);
|
|
1635
|
+
},
|
|
1636
|
+
...(this.isHelperProvider()
|
|
1637
|
+
? { maxMessageBytes: CODEX_APP_SERVER_HELPER_MAX_MESSAGE_BYTES }
|
|
1638
|
+
: {}),
|
|
1639
|
+
requestTimeoutMs: this.options.requestTimeoutMs,
|
|
1640
|
+
WebSocketImpl: this.options.WebSocketImpl
|
|
1641
|
+
});
|
|
1642
|
+
client.setRequestHandler((request) => this.handleServerRequest(request));
|
|
1643
|
+
let initializeResult = null;
|
|
1644
|
+
try {
|
|
1645
|
+
await client.connect();
|
|
1646
|
+
initializeResult = await this.runRequest(
|
|
1647
|
+
() => client.initialize({ clientInfo: this.configuration.clientInfo, ...this.options.initialize }),
|
|
1648
|
+
"codex-app-server-initialize"
|
|
1649
|
+
);
|
|
1650
|
+
await this.authenticateHelperClient(client, runtime);
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
client.close();
|
|
1653
|
+
if (!runtime.runtimeDir) {
|
|
1654
|
+
throw error;
|
|
1655
|
+
}
|
|
1656
|
+
const helper = this.isHelperProvider();
|
|
1657
|
+
const cleanup = await this.host.runtime.cleanupFailedStart(runtime.runtimeDir, {
|
|
1658
|
+
helper,
|
|
1659
|
+
executionId: runtime.executionId,
|
|
1660
|
+
pid: runtime.pid,
|
|
1661
|
+
processIdentity: runtime.processIdentity
|
|
1662
|
+
}, this.host.parameters);
|
|
1663
|
+
this.runtimeStopOwner = cleanup.processExitVerified === true
|
|
1664
|
+
? {
|
|
1665
|
+
...runtime,
|
|
1666
|
+
processExitVerifiedAt: new Date().toISOString(),
|
|
1667
|
+
processState: CODEX_APP_SERVER_PROCESS_STATE.STOPPED
|
|
1668
|
+
}
|
|
1669
|
+
: runtime;
|
|
1670
|
+
this.runtime = null;
|
|
1671
|
+
if (helper) {
|
|
1672
|
+
this.helperAuth = null;
|
|
1673
|
+
this.helperAuthBlocked = true;
|
|
1674
|
+
}
|
|
1675
|
+
if (cleanup.cleanupFailed) {
|
|
1676
|
+
const failure = new Error(
|
|
1677
|
+
helper
|
|
1678
|
+
? "Codex isolated helper runtime could not be retired after startup failed."
|
|
1679
|
+
: "Codex app-server runtime could not be retired after initialization failed.",
|
|
1680
|
+
{ cause: error }
|
|
1681
|
+
);
|
|
1682
|
+
failure.code = helper
|
|
1683
|
+
? `${this.errorPrefix}codex_helper_runtime_cleanup_required`
|
|
1684
|
+
: `${this.errorPrefix}codex_app_server_cleanup_required`;
|
|
1685
|
+
failure.cleanupRequired = true;
|
|
1686
|
+
failure.retryable = false;
|
|
1687
|
+
throw failure;
|
|
1688
|
+
}
|
|
1689
|
+
throw error;
|
|
1690
|
+
}
|
|
1691
|
+
this.client = client;
|
|
1692
|
+
this.planUsage = null;
|
|
1693
|
+
this.planUsagePending = null;
|
|
1694
|
+
client.subscribe((notification = {}) => {
|
|
1695
|
+
if (this.client !== client || this.observationFailure) return;
|
|
1696
|
+
if (notification.method === "account/updated") {
|
|
1697
|
+
this.planUsage = null;
|
|
1698
|
+
this.planUsageAuthGeneration += 1;
|
|
1699
|
+
}
|
|
1700
|
+
if (notification.method === "account/rateLimits/updated") {
|
|
1701
|
+
const limits = notification.params?.rateLimits;
|
|
1702
|
+
if (!limits?.limitId || limits.limitId === "codex") this.planUsage = codexPlanUsage(limits);
|
|
1703
|
+
}
|
|
1704
|
+
this.publishNotification(notification);
|
|
1705
|
+
});
|
|
1706
|
+
this.initializeResult = normalizeCodexAppServerInfo(initializeResult);
|
|
1707
|
+
this.connectionGeneration += 1;
|
|
1708
|
+
return {
|
|
1709
|
+
initializeResult: this.isHelperProvider() ? this.initializeResult : initializeResult,
|
|
1710
|
+
runtime
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
currentConnectionGeneration() {
|
|
1715
|
+
return this.connectionGeneration;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
publishNotification(notification) {
|
|
1719
|
+
const threadId = notification.params?.threadId;
|
|
1720
|
+
const turnId = notification.params?.turnId;
|
|
1721
|
+
const item = notification.params?.item;
|
|
1722
|
+
if (item?.type === "commandExecution" && item.processId && item.source === "unifiedExecStartup") {
|
|
1723
|
+
const key = JSON.stringify([threadId, turnId, item.id]);
|
|
1724
|
+
if (notification.method === "item/completed") {
|
|
1725
|
+
const command = this.commandExecutions.get(key);
|
|
1726
|
+
if (command) command.completed = true;
|
|
1727
|
+
this.commandExecutions.delete(key);
|
|
1728
|
+
} else if (notification.method === "item/started") {
|
|
1729
|
+
const command = this.commandExecutions.get(key) || {
|
|
1730
|
+
threadId, turnId, itemId: item.id, processId: item.processId, client: this.client
|
|
1731
|
+
};
|
|
1732
|
+
this.commandExecutions.set(key, command);
|
|
1733
|
+
// Native startup may finish after turn/interrupt has already returned.
|
|
1734
|
+
if (this.interruptedTurns.has(JSON.stringify([threadId, turnId]))) {
|
|
1735
|
+
void this.#stopCommand(command).catch(() => null);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
const probe = this.threadControlProbes.get(threadId);
|
|
1740
|
+
const method = notification.method;
|
|
1741
|
+
if (probe && (/^(turn|item)\//u.test(method) || method === "thread/status/changed")) {
|
|
1742
|
+
const turnId = notification.params?.turnId || notification.params?.turn?.id;
|
|
1743
|
+
if (method === "turn/started" && probe.turnId && probe.turnId !== turnId) {
|
|
1744
|
+
this.threadControlProbes.delete(threadId);
|
|
1745
|
+
} else if (probe.verified) {
|
|
1746
|
+
if (turnId === probe.turnId) {
|
|
1747
|
+
if (method === "item/completed" && notification.params.item?.id === probe.itemId) {
|
|
1748
|
+
probe.result = notification.params.item;
|
|
1749
|
+
}
|
|
1750
|
+
if (method === "turn/completed") probe.completed = true;
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
} else {
|
|
1754
|
+
if (method === "turn/started") probe.turnId = turnId;
|
|
1755
|
+
probe.pending.push(notification);
|
|
1756
|
+
if (method === "item/started" || probe.pending.length >= 32) {
|
|
1757
|
+
const item = notification.params?.item;
|
|
1758
|
+
if (item?.type === "commandExecution" && item.command?.includes(probe.marker)) {
|
|
1759
|
+
probe.verified = true;
|
|
1760
|
+
probe.itemId = item.id;
|
|
1761
|
+
probe.pending = [];
|
|
1762
|
+
} else {
|
|
1763
|
+
// A concurrent native user turn must retain every event. Only a
|
|
1764
|
+
// positively identified control probe may be hidden from chat.
|
|
1765
|
+
this.threadControlProbes.delete(threadId);
|
|
1766
|
+
for (const pending of probe.pending) this.publishNotification(pending);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
for (const subscriber of this.notificationSubscribers) {
|
|
1773
|
+
try {
|
|
1774
|
+
subscriber(notification);
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
this.failObservation(error);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
currentServerInfo() {
|
|
1782
|
+
return this.initializeResult;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
isControlProbeTurn(threadId, turnId) {
|
|
1786
|
+
const probe = this.threadControlProbes.get(threadId);
|
|
1787
|
+
return Boolean(turnId && probe?.verified && probe.turnId === turnId);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
async currentRuntimeInfo() {
|
|
1791
|
+
const runtime = this.runtime || {};
|
|
1792
|
+
const executionMode = codexAppServerExecutionMode(this.options);
|
|
1793
|
+
const parameters = this.host.parameters;
|
|
1794
|
+
return Object.freeze({
|
|
1795
|
+
accountIdentitySignature: await this.credentials.identity(this.host.parameters, {
|
|
1796
|
+
includeInteractive: true
|
|
1797
|
+
}),
|
|
1798
|
+
authStateSignature: await this.credentials.generation(parameters),
|
|
1799
|
+
endpoint: normalizeAgentText(runtime.endpoint),
|
|
1800
|
+
executionMode,
|
|
1801
|
+
executionContextHash: normalizeAgentText(runtime.executionContextHash) ||
|
|
1802
|
+
normalizeAgentText(parameters.compatibility?.executionContextHash),
|
|
1803
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
1804
|
+
runtimeDir: normalizeAgentText(
|
|
1805
|
+
runtime.runtimeDir || parameters.runtimeDir
|
|
1806
|
+
),
|
|
1807
|
+
runtimesHash: normalizeAgentText(runtime.runtimesHash) ||
|
|
1808
|
+
normalizeAgentText(parameters.compatibility?.runtimesHash),
|
|
1809
|
+
terminalEnvHash: normalizeAgentText(runtime.terminalEnvHash) ||
|
|
1810
|
+
codexAppServerTerminalEnvHash(this.isHelperProvider() ? {} : this.options.terminalEnv),
|
|
1811
|
+
toolHomeSource: executionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
|
|
1812
|
+
? ""
|
|
1813
|
+
: normalizeAgentText(this.options.toolHomeSource),
|
|
1814
|
+
transport: normalizeAgentText(runtime.transport) || CODEX_APP_SERVER_TRANSPORT.UNIX
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
async currentHelperExecutionContext() {
|
|
1819
|
+
const runtime = await this.ensureRuntime();
|
|
1820
|
+
const cwd = normalizeAgentText(this.options.helperWorkdir)
|
|
1821
|
+
? path.resolve(this.options.helperWorkdir)
|
|
1822
|
+
: codexAppServerHelperWorkspaceDir(runtime.runtimeDir);
|
|
1823
|
+
if (!runtime.runtimeDir) {
|
|
1824
|
+
throw this.accountReader.error(
|
|
1825
|
+
`${this.errorPrefix}codex_helper_runtime_invalid`,
|
|
1826
|
+
"Codex helper runtime isolation could not be verified."
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
await ensureWritablePrivateDirectory(cwd);
|
|
1830
|
+
return Object.freeze({
|
|
1831
|
+
accountIdentitySignature: await this.credentials.identity(this.host.parameters, {
|
|
1832
|
+
includeInteractive: true
|
|
1833
|
+
}),
|
|
1834
|
+
cwd,
|
|
1835
|
+
executionMode: CODEX_APP_SERVER_EXECUTION_MODES.HELPER
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
isAvailable() {
|
|
1840
|
+
return Boolean(this.client?.isOpen?.() && this.runtime);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
async activeClient() {
|
|
1844
|
+
if (this.observationFailure) throw this.observationFailure;
|
|
1845
|
+
if (this.client?.isOpen?.()) {
|
|
1846
|
+
return this.client;
|
|
1847
|
+
}
|
|
1848
|
+
await this.connect();
|
|
1849
|
+
return this.client;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
async ensureAvailable({ modelProviderId = "" } = {}) {
|
|
1853
|
+
if (this.observationFailure) throw this.observationFailure;
|
|
1854
|
+
if (this.isAvailable()) {
|
|
1855
|
+
return {
|
|
1856
|
+
client: this.client,
|
|
1857
|
+
ok: true,
|
|
1858
|
+
reusedClient: true,
|
|
1859
|
+
runtime: this.runtime
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
if (this.availabilityPromise) {
|
|
1863
|
+
return this.availabilityPromise;
|
|
1864
|
+
}
|
|
1865
|
+
const operation = (async () => {
|
|
1866
|
+
if (!this.isHelperProvider()) {
|
|
1867
|
+
await this.preflightAuth("codex-app-server-ensure-available", modelProviderId);
|
|
1868
|
+
}
|
|
1869
|
+
const client = await this.activeClient();
|
|
1870
|
+
return {
|
|
1871
|
+
client,
|
|
1872
|
+
ok: true,
|
|
1873
|
+
runtime: this.runtime
|
|
1874
|
+
};
|
|
1875
|
+
})();
|
|
1876
|
+
this.availabilityPromise = operation;
|
|
1877
|
+
try {
|
|
1878
|
+
return await operation;
|
|
1879
|
+
} finally {
|
|
1880
|
+
if (this.availabilityPromise === operation) {
|
|
1881
|
+
this.availabilityPromise = null;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
async readPlanUsage() {
|
|
1887
|
+
const client = this.client;
|
|
1888
|
+
if (!client?.isOpen?.()) return { status: "unavailable", windows: [] };
|
|
1889
|
+
if (this.planUsage && Date.now() - this.planUsage.checkedAt < 60_000) return this.planUsage;
|
|
1890
|
+
if (this.planUsagePending) return this.planUsagePending;
|
|
1891
|
+
const previous = this.planUsage;
|
|
1892
|
+
const authGeneration = this.planUsageAuthGeneration;
|
|
1893
|
+
const signal = AbortSignal.timeout(10_000);
|
|
1894
|
+
const operation = (async () => {
|
|
1895
|
+
try {
|
|
1896
|
+
const account = await client.request("account/read", { refreshToken: false }, { signal });
|
|
1897
|
+
if (this.client !== client || this.planUsageAuthGeneration !== authGeneration) return { status: "unavailable", windows: [] };
|
|
1898
|
+
if (account?.account?.type !== "chatgpt") {
|
|
1899
|
+
this.planUsage = { status: "unsupported", windows: [], checkedAt: Date.now() };
|
|
1900
|
+
} else {
|
|
1901
|
+
const result = await client.request("account/rateLimits/read", {}, { signal });
|
|
1902
|
+
if (this.client !== client || this.planUsageAuthGeneration !== authGeneration) return { status: "unavailable", windows: [] };
|
|
1903
|
+
// A live event received during the read is newer than this request.
|
|
1904
|
+
if (this.planUsage === previous) this.planUsage = codexPlanUsage(result.rateLimitsByLimitId?.codex || result.rateLimits);
|
|
1905
|
+
}
|
|
1906
|
+
return this.planUsage;
|
|
1907
|
+
} catch {
|
|
1908
|
+
return { status: "unavailable", windows: [] };
|
|
1909
|
+
}
|
|
1910
|
+
})();
|
|
1911
|
+
this.planUsagePending = operation;
|
|
1912
|
+
try { return await operation; }
|
|
1913
|
+
finally { if (this.planUsagePending === operation) this.planUsagePending = null; }
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
subscribe(callback) {
|
|
1917
|
+
if (typeof callback !== "function") {
|
|
1918
|
+
throw new TypeError("Codex app-server observer must be a function.");
|
|
1919
|
+
}
|
|
1920
|
+
// Observation belongs to this provider, not one replaceable socket.
|
|
1921
|
+
this.notificationSubscribers.add(callback);
|
|
1922
|
+
return () => this.notificationSubscribers.delete(callback);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
failObservation(cause) {
|
|
1926
|
+
const message = cause?.code === `${this.errorPrefix}agent_control_recovery_failed`
|
|
1927
|
+
? cause.message
|
|
1928
|
+
: "Codex observation failed. Work must be stopped before it can continue.";
|
|
1929
|
+
this.observationFailure ||= Object.assign(
|
|
1930
|
+
new Error(message, { cause }),
|
|
1931
|
+
{ code: `${this.errorPrefix}codex_observation_lost` }
|
|
1932
|
+
);
|
|
1933
|
+
return this.options.onObservationLost?.(this.observationFailure);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
async stopThreadForObservationLoss(threadId, turnId) {
|
|
1937
|
+
// Control uses only this existing socket. It must not spawn or resume work
|
|
1938
|
+
// to find out whether the work we lost sight of has stopped.
|
|
1939
|
+
const client = this.client;
|
|
1940
|
+
if (!client?.isOpen()) throw new Error("Codex control connection is unavailable.");
|
|
1941
|
+
if (this.commandStopFailure) throw this.commandStopFailure;
|
|
1942
|
+
const signal = AbortSignal.timeout(5_000);
|
|
1943
|
+
const request = (method, params) => client.request(method, params, { signal });
|
|
1944
|
+
let { goal } = await request("thread/goal/get", { threadId });
|
|
1945
|
+
if (goal?.status === "active") {
|
|
1946
|
+
const result = await request("thread/goal/set", { threadId, status: "paused" });
|
|
1947
|
+
if (result.goal?.status !== "paused" || result.goal?.createdAt !== goal.createdAt || result.goal?.objective !== goal.objective) {
|
|
1948
|
+
throw new Error("Codex did not confirm that the same goal was paused.");
|
|
1949
|
+
}
|
|
1950
|
+
goal = result.goal;
|
|
1951
|
+
}
|
|
1952
|
+
const readStatus = async () => {
|
|
1953
|
+
const { thread } = await request("thread/read", { threadId, includeTurns: false });
|
|
1954
|
+
return typeof thread?.status === "string" ? thread.status : thread?.status?.type;
|
|
1955
|
+
};
|
|
1956
|
+
if (!["idle", "notLoaded"].includes(await readStatus())) {
|
|
1957
|
+
if (!turnId) {
|
|
1958
|
+
const page = await request("thread/turns/list", { threadId, limit: 1, sortDirection: "desc", itemsView: "summary" });
|
|
1959
|
+
turnId = page.data?.[0]?.id;
|
|
1960
|
+
}
|
|
1961
|
+
if (!turnId) throw new Error("Codex's running turn could not be identified.");
|
|
1962
|
+
await this.#interruptTurn(client, threadId, turnId, signal);
|
|
1963
|
+
if (await readStatus() !== "idle") throw new Error("Codex did not confirm that its turn stopped.");
|
|
1964
|
+
}
|
|
1965
|
+
return { goal };
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
setServerRequestHandler(callback) {
|
|
1969
|
+
const handler = typeof callback === "function" ? callback : null;
|
|
1970
|
+
this.serverRequestHandler = handler;
|
|
1971
|
+
this.client?.setRequestHandler?.((request) => this.handleServerRequest(request));
|
|
1972
|
+
return () => {
|
|
1973
|
+
if (this.serverRequestHandler === handler) {
|
|
1974
|
+
this.serverRequestHandler = null;
|
|
1975
|
+
this.client?.setRequestHandler?.((request) => this.handleServerRequest(request));
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
async startThread({ hostContext, ...params } = {}) {
|
|
1981
|
+
params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext });
|
|
1982
|
+
delete params.hostContext;
|
|
1983
|
+
params = await this.options.prepareThreadParams?.(params) || params;
|
|
1984
|
+
const client = await this.activeClient();
|
|
1985
|
+
params = await this.withHistoryAdapter(params, client);
|
|
1986
|
+
const environment = this.options.prepareThreadEnvironment
|
|
1987
|
+
? await this.options.prepareThreadEnvironment(this.options.threadEnv || {})
|
|
1988
|
+
: this.options.threadEnv;
|
|
1989
|
+
const requestParams = this.conversationRuntime.threadParameters(params, environment);
|
|
1990
|
+
if (params.ephemeral !== true) requestParams.historyMode = "paginated";
|
|
1991
|
+
const response = await this.runRequest(
|
|
1992
|
+
() => client.request("thread/start", requestParams),
|
|
1993
|
+
"codex-app-server-thread-start"
|
|
1994
|
+
);
|
|
1995
|
+
if (hostContext) {
|
|
1996
|
+
await this.options.bindThreadContext(response?.thread?.id, hostContext, params);
|
|
1997
|
+
}
|
|
1998
|
+
// Native ephemeral helpers have no stored history and report "legacy".
|
|
1999
|
+
// Paginated history is required only for persistent conversations.
|
|
2000
|
+
if (params.ephemeral !== true && response?.thread?.historyMode === "legacy") {
|
|
2001
|
+
throw Object.assign(new Error("Codex did not create the required paginated conversation history. Update Codex before starting a conversation."), {
|
|
2002
|
+
code: `${this.errorPrefix}codex_history_unsupported`
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
if (response?.thread?.historyMode === "paginated" && response.thread.ephemeral !== true) {
|
|
2006
|
+
const threadId = normalizeAgentText(response.thread.id);
|
|
2007
|
+
// A new paginated thread needs its metadata and empty rollout persisted
|
|
2008
|
+
// before it can be resumed. This initializes only the just-created,
|
|
2009
|
+
// empty thread; it never hydrates an existing conversation.
|
|
2010
|
+
await this.runRequest(async () => {
|
|
2011
|
+
await client.request("thread/name/set", { threadId, name: threadId });
|
|
2012
|
+
await client.request("thread/read", { threadId, includeTurns: true });
|
|
2013
|
+
}, "codex-app-server-thread-initialize-history");
|
|
2014
|
+
}
|
|
2015
|
+
if (this.options.prepareThreadEnvironment && response?.thread?.id) {
|
|
2016
|
+
this.conversationRuntime.registerThread(response.thread.id, {
|
|
2017
|
+
client, executionId: normalizeAgentText(this.runtime?.executionId),
|
|
2018
|
+
params: requestParams, managed: !Object.hasOwn(params.config || {}, "shell_environment_policy")
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return {
|
|
2022
|
+
...normalizeAgentThread({
|
|
2023
|
+
id: response?.thread?.id,
|
|
2024
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2025
|
+
raw: response?.thread
|
|
2026
|
+
}),
|
|
2027
|
+
response
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
async resumeThread(threadId = "", { hostContext, ...params } = {}) {
|
|
2032
|
+
if (hostContext) await this.options.bindThreadContext(threadId, hostContext, params);
|
|
2033
|
+
params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext }, threadId);
|
|
2034
|
+
delete params.hostContext;
|
|
2035
|
+
params = await this.options.prepareThreadParams?.(params) || params;
|
|
2036
|
+
await this.options.beforeResumeThread?.(threadId);
|
|
2037
|
+
const client = await this.activeClient();
|
|
2038
|
+
let response;
|
|
2039
|
+
if (this.options.prepareThreadEnvironment) {
|
|
2040
|
+
response = await this.withThreadEnvironment(threadId, params, (requestParams, resumed) =>
|
|
2041
|
+
resumed || client.request("thread/resume", { excludeTurns: true, ...requestParams, threadId }));
|
|
2042
|
+
} else {
|
|
2043
|
+
params = await this.withHistoryAdapter(params, client, { threadId });
|
|
2044
|
+
const requestParams = codexAppServerThreadRequestParams(params, this.options.threadEnv);
|
|
2045
|
+
response = await this.runRequest(
|
|
2046
|
+
() => client.request("thread/resume", {
|
|
2047
|
+
excludeTurns: true,
|
|
2048
|
+
...requestParams,
|
|
2049
|
+
threadId: normalizeAgentText(threadId || requestParams.threadId)
|
|
2050
|
+
}),
|
|
2051
|
+
"codex-app-server-thread-resume"
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
return {
|
|
2055
|
+
...normalizeAgentThread({
|
|
2056
|
+
id: response?.thread?.id,
|
|
2057
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2058
|
+
raw: response?.thread
|
|
2059
|
+
}),
|
|
2060
|
+
response
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async withHistoryAdapter(params, client, options = {}) {
|
|
2065
|
+
return prepareCodexHistory(params, client, { ...options,
|
|
2066
|
+
baseUrl: this.runtime?.historyAdapterBaseUrl, modelProviderId: this.options.modelProviderId });
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
async withThreadEnvironment(threadId, params, operation) {
|
|
2070
|
+
try {
|
|
2071
|
+
return await this.conversationRuntime.withThreadContext(threadId, params, operation);
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if (typeof error.code === "string" && error.code.startsWith("assistant_")) error.code = `${this.errorPrefix}${error.code.slice(10)}`;
|
|
2074
|
+
throw error;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
async #verifyThreadEnvironment(threadId, environment, request, signal, { requiredKeys = [] } = {}) {
|
|
2079
|
+
const keys = Object.keys(environment).filter((key) => key.startsWith(this.configuration.controlEnvironmentPrefix) || requiredKeys.includes(key)).sort();
|
|
2080
|
+
if (!keys.length) return;
|
|
2081
|
+
|
|
2082
|
+
const marker = `${this.configuration.controlProbePrefix}${randomUUID()}:`;
|
|
2083
|
+
const probe = { marker, pending: [], verified: false, turnId: "" };
|
|
2084
|
+
this.threadControlProbes.set(threadId, probe);
|
|
2085
|
+
const expected = createHash("sha256").update(JSON.stringify(keys.map((key) => [key, environment[key]]))).digest("hex");
|
|
2086
|
+
// This bounded native shell check uses no model and prints only a digest,
|
|
2087
|
+
// never credentials or environment values.
|
|
2088
|
+
await request("thread/shellCommand", {
|
|
2089
|
+
threadId, command: codexControlProbeCommand(keys, marker), timeoutMs: 2000
|
|
2090
|
+
});
|
|
2091
|
+
while (true) {
|
|
2092
|
+
signal.throwIfAborted();
|
|
2093
|
+
// The native completion event owns this result; saved history is not a
|
|
2094
|
+
// second acknowledgement of this live control operation.
|
|
2095
|
+
if (probe.completed && probe.result) {
|
|
2096
|
+
if (probe.result.type !== "commandExecution" || probe.result.exitCode !== 0 ||
|
|
2097
|
+
probe.result.aggregatedOutput?.trim() !== `${marker}${expected}`) {
|
|
2098
|
+
throw Object.assign(new Error("Another native subscriber retained the previous assistant settings. Close the native assistant terminal and retry Resume."), {
|
|
2099
|
+
code: "assistant_agent_control_binding_retained"
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
async ensureThreadControls(threadId) {
|
|
2109
|
+
if (!this.options.prepareThreadEnvironment) return;
|
|
2110
|
+
return this.withThreadEnvironment(threadId, {}, async (_params, resumed) => ({ ok: true, recovered: Boolean(resumed) }));
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
async readGoal(threadId = "", { signal } = {}) {
|
|
2114
|
+
const client = await this.activeClient();
|
|
2115
|
+
return this.runRequest(
|
|
2116
|
+
() => client.request("thread/goal/get", { threadId: normalizeAgentText(threadId) }, { signal }),
|
|
2117
|
+
"codex-app-server-goal-read"
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
async setGoal(threadId, { objective, tokenBudget } = {}, { signal } = {}) {
|
|
2122
|
+
if (typeof objective !== "string" || !objective.trim() ||
|
|
2123
|
+
(tokenBudget !== undefined && (!Number.isSafeInteger(tokenBudget) || tokenBudget <= 0))) {
|
|
2124
|
+
throw new TypeError("A Codex goal requires an objective and an optional positive token budget.");
|
|
2125
|
+
}
|
|
2126
|
+
this.conversationRuntime.goalChanged(threadId);
|
|
2127
|
+
const operation = async () => {
|
|
2128
|
+
const client = await this.activeClient();
|
|
2129
|
+
return client.request("thread/goal/set", {
|
|
2130
|
+
threadId: normalizeAgentText(threadId), objective: objective.trim(), status: "active",
|
|
2131
|
+
...(tokenBudget === undefined ? {} : { tokenBudget })
|
|
2132
|
+
}, { signal });
|
|
2133
|
+
};
|
|
2134
|
+
return this.options.prepareThreadEnvironment
|
|
2135
|
+
? this.withThreadEnvironment(threadId, {}, operation)
|
|
2136
|
+
: this.runRequest(operation, "codex-app-server-goal-set");
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
async setGoalStatus(threadId = "", status = "", { signal } = {}) {
|
|
2140
|
+
if (!["active", "paused"].includes(status)) {
|
|
2141
|
+
throw new TypeError("Invalid Codex goal status.");
|
|
2142
|
+
}
|
|
2143
|
+
this.conversationRuntime.goalChanged(threadId);
|
|
2144
|
+
const operation = async () => {
|
|
2145
|
+
const client = await this.activeClient();
|
|
2146
|
+
return client.request("thread/goal/set", { threadId: normalizeAgentText(threadId), status }, { signal });
|
|
2147
|
+
};
|
|
2148
|
+
return status === "active" && this.options.prepareThreadEnvironment
|
|
2149
|
+
? this.withThreadEnvironment(threadId, {}, operation)
|
|
2150
|
+
: this.runRequest(operation, "codex-app-server-goal-status");
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
async clearGoal(threadId = "", { signal } = {}) {
|
|
2154
|
+
this.conversationRuntime.goalChanged(threadId);
|
|
2155
|
+
const client = await this.activeClient();
|
|
2156
|
+
return this.runRequest(
|
|
2157
|
+
() => client.request("thread/goal/clear", { threadId: normalizeAgentText(threadId) }, { signal }),
|
|
2158
|
+
"codex-app-server-goal-clear"
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
async readThread(threadId = "") {
|
|
2163
|
+
const client = await this.activeClient();
|
|
2164
|
+
const response = await this.runRequest(
|
|
2165
|
+
() => client.request("thread/read", {
|
|
2166
|
+
includeTurns: true,
|
|
2167
|
+
threadId: normalizeAgentText(threadId)
|
|
2168
|
+
}),
|
|
2169
|
+
"codex-app-server-thread-read"
|
|
2170
|
+
);
|
|
2171
|
+
return {
|
|
2172
|
+
...normalizeAgentThread({
|
|
2173
|
+
id: response?.thread?.id || threadId,
|
|
2174
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2175
|
+
raw: response?.thread || response
|
|
2176
|
+
}),
|
|
2177
|
+
response
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
async readThreadStatus(threadId = "") {
|
|
2182
|
+
const client = await this.activeClient();
|
|
2183
|
+
const response = await this.runRequest(
|
|
2184
|
+
() => client.request("thread/read", {
|
|
2185
|
+
includeTurns: false,
|
|
2186
|
+
threadId: normalizeAgentText(threadId)
|
|
2187
|
+
}),
|
|
2188
|
+
"codex-app-server-thread-status"
|
|
2189
|
+
);
|
|
2190
|
+
return {
|
|
2191
|
+
...normalizeAgentThread({
|
|
2192
|
+
id: response?.thread?.id || threadId,
|
|
2193
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2194
|
+
raw: response?.thread || response
|
|
2195
|
+
}),
|
|
2196
|
+
response
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
async listThreadTurns(threadId = "", params = {}) {
|
|
2201
|
+
const client = await this.activeClient();
|
|
2202
|
+
return this.runRequest(
|
|
2203
|
+
() => client.request("thread/turns/list", {
|
|
2204
|
+
...params,
|
|
2205
|
+
threadId: normalizeAgentText(threadId || params.threadId)
|
|
2206
|
+
}),
|
|
2207
|
+
"codex-app-server-thread-turns-list"
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
async exportThreadHistory(threadId, onRecord, { signal } = {}) {
|
|
2212
|
+
const active = await this.activeClient();
|
|
2213
|
+
// Oversized native items must fail preservation without disconnecting the
|
|
2214
|
+
// shared conversation observer. Reuse JSKIT's transport on a separate socket.
|
|
2215
|
+
const client = new CodexAppServerJsonRpcClient({
|
|
2216
|
+
endpoint: active.endpoint,
|
|
2217
|
+
maxMessageBytes: 64 * 1024 ** 2,
|
|
2218
|
+
requestTimeoutMs: 30_000,
|
|
2219
|
+
WebSocketImpl: this.options.WebSocketImpl
|
|
2220
|
+
});
|
|
2221
|
+
const deadline = AbortSignal.timeout(300_000);
|
|
2222
|
+
const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
2223
|
+
boundedSignal.throwIfAborted();
|
|
2224
|
+
const abort = () => client.close();
|
|
2225
|
+
boundedSignal.addEventListener("abort", abort, { once: true });
|
|
2226
|
+
try {
|
|
2227
|
+
await client.connect();
|
|
2228
|
+
boundedSignal.throwIfAborted();
|
|
2229
|
+
await client.initialize({ clientInfo: this.configuration.historyClientInfo });
|
|
2230
|
+
boundedSignal.throwIfAborted();
|
|
2231
|
+
return await exportCodexNativeHistory(client, normalizeAgentText(threadId), onRecord, { signal: boundedSignal, errorPrefix: this.errorPrefix });
|
|
2232
|
+
} finally {
|
|
2233
|
+
boundedSignal.removeEventListener("abort", abort);
|
|
2234
|
+
client.close();
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
async listLoadedThreads(params = {}) {
|
|
2239
|
+
const client = await this.activeClient();
|
|
2240
|
+
return this.runRequest(
|
|
2241
|
+
() => client.request("thread/loaded/list", params),
|
|
2242
|
+
"codex-app-server-thread-loaded-list"
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
async listAppServerThreadsForCwd({
|
|
2247
|
+
cwd = "",
|
|
2248
|
+
signal = null
|
|
2249
|
+
} = {}) {
|
|
2250
|
+
const normalizedCwd = normalizeAgentText(cwd);
|
|
2251
|
+
if (!normalizedCwd || codexAppServerTextHasControlCharacters(normalizedCwd)) {
|
|
2252
|
+
const error = new Error("Codex session thread inventory requires an exact cwd.");
|
|
2253
|
+
error.code = `${this.errorPrefix}codex_session_thread_inventory_invalid`;
|
|
2254
|
+
throw error;
|
|
2255
|
+
}
|
|
2256
|
+
const client = await this.activeClient();
|
|
2257
|
+
const state = await listBoundedCodexAppServerThreadIds({
|
|
2258
|
+
archived: false,
|
|
2259
|
+
client,
|
|
2260
|
+
cwd: normalizedCwd,
|
|
2261
|
+
errorCode: `${this.errorPrefix}codex_session_thread_inventory_invalid`,
|
|
2262
|
+
label: "session",
|
|
2263
|
+
requestLabel: "codex-app-server-session-thread-list",
|
|
2264
|
+
runRequest: this.runRequest.bind(this),
|
|
2265
|
+
signal,
|
|
2266
|
+
state: {
|
|
2267
|
+
entryCount: 0,
|
|
2268
|
+
threadIds: new Set(),
|
|
2269
|
+
totalBytes: 0
|
|
2270
|
+
},
|
|
2271
|
+
verifyCwd: true
|
|
2272
|
+
});
|
|
2273
|
+
return Object.freeze({
|
|
2274
|
+
cwd: normalizedCwd,
|
|
2275
|
+
threadIds: Object.freeze([...state.threadIds].sort())
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// Native thread/delete cascades through all spawned descendants, including
|
|
2280
|
+
// archived and non-app-server threads. Never infer its scope from cwd alone.
|
|
2281
|
+
async listThreadDescendants(threadId) {
|
|
2282
|
+
const ancestorThreadId = normalizeAgentText(threadId);
|
|
2283
|
+
if (!ancestorThreadId || ancestorThreadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
|
|
2284
|
+
codexAppServerTextHasControlCharacters(ancestorThreadId)) {
|
|
2285
|
+
throw new TypeError("Codex descendant inventory requires an exact thread id.");
|
|
2286
|
+
}
|
|
2287
|
+
const client = await this.activeClient();
|
|
2288
|
+
const state = { entryCount: 0, threadIds: new Set(), totalBytes: 0 };
|
|
2289
|
+
for (const archived of [false, true]) {
|
|
2290
|
+
await listBoundedCodexAppServerThreadIds({
|
|
2291
|
+
ancestorThreadId, archived, client, state,
|
|
2292
|
+
sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
|
|
2293
|
+
errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "retirement",
|
|
2294
|
+
requestLabel: "codex-app-server-descendant-list", runRequest: this.runRequest.bind(this)
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
return [...state.threadIds].sort();
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
async listNativeThreadsForCwd(cwd) {
|
|
2301
|
+
if (!path.isAbsolute(cwd || "") || codexAppServerTextHasControlCharacters(cwd)) throw new TypeError("Native inventory requires an exact absolute directory.");
|
|
2302
|
+
const client = await this.activeClient();
|
|
2303
|
+
const state = { entryCount: 0, threadIds: new Set(), totalBytes: 0 };
|
|
2304
|
+
for (const archived of [false, true]) {
|
|
2305
|
+
await listBoundedCodexAppServerThreadIds({ archived, client, cwd, state, verifyCwd: true,
|
|
2306
|
+
sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
|
|
2307
|
+
errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "native storage",
|
|
2308
|
+
requestLabel: "codex-app-server-native-storage-list", runRequest: this.runRequest.bind(this) });
|
|
2309
|
+
}
|
|
2310
|
+
return [...state.threadIds].sort();
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
async nativeThreadExists(threadId, { signal } = {}) {
|
|
2314
|
+
const id = normalizeAgentText(threadId);
|
|
2315
|
+
if (!id || id.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH || codexAppServerTextHasControlCharacters(id)) {
|
|
2316
|
+
throw new TypeError("Native existence checks require an exact thread id.");
|
|
2317
|
+
}
|
|
2318
|
+
const client = await this.activeClient();
|
|
2319
|
+
const state = { entryCount: 0, threadIds: new Set(), totalBytes: 0 };
|
|
2320
|
+
for (const archived of [false, true]) {
|
|
2321
|
+
await listBoundedCodexAppServerThreadIds({ archived, client, state, signal,
|
|
2322
|
+
sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
|
|
2323
|
+
errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "native storage",
|
|
2324
|
+
requestLabel: "codex-app-server-native-existence", runRequest: this.runRequest.bind(this) });
|
|
2325
|
+
if (state.threadIds.has(id)) return true;
|
|
2326
|
+
}
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
async listHelperThreads({
|
|
2331
|
+
signal = null
|
|
2332
|
+
} = {}) {
|
|
2333
|
+
const client = await this.activeClient();
|
|
2334
|
+
const execution = await this.currentHelperExecutionContext();
|
|
2335
|
+
const state = {
|
|
2336
|
+
entryCount: 0,
|
|
2337
|
+
threadIds: new Set(),
|
|
2338
|
+
totalBytes: 0
|
|
2339
|
+
};
|
|
2340
|
+
for (const archived of [false, true]) {
|
|
2341
|
+
await listBoundedCodexAppServerThreadIds({
|
|
2342
|
+
archived,
|
|
2343
|
+
client,
|
|
2344
|
+
cwd: execution.cwd,
|
|
2345
|
+
errorCode: `${this.errorPrefix}codex_helper_thread_inventory_invalid`,
|
|
2346
|
+
label: "helper",
|
|
2347
|
+
requestLabel: "codex-app-server-helper-thread-list",
|
|
2348
|
+
runRequest: this.runRequest.bind(this),
|
|
2349
|
+
signal,
|
|
2350
|
+
state
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
return Object.freeze({
|
|
2354
|
+
threadIds: Object.freeze([...state.threadIds].sort())
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
async listModels(params = {}, {
|
|
2359
|
+
signal = null
|
|
2360
|
+
} = {}) {
|
|
2361
|
+
const client = await this.activeClient();
|
|
2362
|
+
const data = [];
|
|
2363
|
+
const seenCursors = new Set();
|
|
2364
|
+
const includeHidden = params.includeHidden === true;
|
|
2365
|
+
const limit = normalizePositiveInteger(
|
|
2366
|
+
params.limit,
|
|
2367
|
+
CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT
|
|
2368
|
+
);
|
|
2369
|
+
const boundedLimit = Math.min(limit, CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT);
|
|
2370
|
+
let totalBytes = 0;
|
|
2371
|
+
let cursor = params.cursor === undefined || params.cursor === null || params.cursor === ""
|
|
2372
|
+
? ""
|
|
2373
|
+
: typeof params.cursor === "string"
|
|
2374
|
+
? params.cursor.trim()
|
|
2375
|
+
: null;
|
|
2376
|
+
if (cursor === null || cursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH) {
|
|
2377
|
+
throw this.modelCatalogError("Codex model catalog cursor is invalid.");
|
|
2378
|
+
}
|
|
2379
|
+
for (let page = 0; page < CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES; page += 1) {
|
|
2380
|
+
if (cursor && seenCursors.has(cursor)) {
|
|
2381
|
+
throw this.modelCatalogError("Codex model catalog repeated a pagination cursor.");
|
|
2382
|
+
}
|
|
2383
|
+
if (cursor) {
|
|
2384
|
+
seenCursors.add(cursor);
|
|
2385
|
+
}
|
|
2386
|
+
const response = await this.runRequest(
|
|
2387
|
+
() => client.request("model/list", {
|
|
2388
|
+
...(cursor ? { cursor } : {}),
|
|
2389
|
+
includeHidden,
|
|
2390
|
+
limit: boundedLimit
|
|
2391
|
+
}, { signal }),
|
|
2392
|
+
"codex-app-server-model-list"
|
|
2393
|
+
);
|
|
2394
|
+
if (!Array.isArray(response?.data)) {
|
|
2395
|
+
throw this.modelCatalogError(
|
|
2396
|
+
"Codex model catalog response did not contain a data array."
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
if (
|
|
2400
|
+
response.data.length > boundedLimit ||
|
|
2401
|
+
data.length + response.data.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRIES
|
|
2402
|
+
) {
|
|
2403
|
+
throw this.modelCatalogError("Codex model catalog exceeded its entry limit.");
|
|
2404
|
+
}
|
|
2405
|
+
for (const entry of response.data) {
|
|
2406
|
+
if (!isPlainObject(entry)) {
|
|
2407
|
+
throw this.modelCatalogError("Codex model catalog contained an invalid entry.");
|
|
2408
|
+
}
|
|
2409
|
+
let entryBytes = 0;
|
|
2410
|
+
try {
|
|
2411
|
+
entryBytes = Buffer.byteLength(JSON.stringify(entry), "utf8");
|
|
2412
|
+
} catch {
|
|
2413
|
+
throw this.modelCatalogError("Codex model catalog contained an invalid entry.");
|
|
2414
|
+
}
|
|
2415
|
+
if (
|
|
2416
|
+
entryBytes > CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRY_BYTES ||
|
|
2417
|
+
totalBytes + entryBytes > CODEX_APP_SERVER_MODEL_CATALOG_MAX_TOTAL_BYTES
|
|
2418
|
+
) {
|
|
2419
|
+
throw this.modelCatalogError("Codex model catalog exceeded its response limit.");
|
|
2420
|
+
}
|
|
2421
|
+
totalBytes += entryBytes;
|
|
2422
|
+
data.push(entry);
|
|
2423
|
+
}
|
|
2424
|
+
const nextCursor = response.nextCursor === undefined || response.nextCursor === null || response.nextCursor === ""
|
|
2425
|
+
? ""
|
|
2426
|
+
: typeof response.nextCursor === "string"
|
|
2427
|
+
? response.nextCursor.trim()
|
|
2428
|
+
: null;
|
|
2429
|
+
if (nextCursor === null || nextCursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH) {
|
|
2430
|
+
throw this.modelCatalogError("Codex model catalog cursor is invalid.");
|
|
2431
|
+
}
|
|
2432
|
+
if (!nextCursor) {
|
|
2433
|
+
return {
|
|
2434
|
+
data,
|
|
2435
|
+
nextCursor: null
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
if (seenCursors.has(nextCursor)) {
|
|
2439
|
+
throw this.modelCatalogError("Codex model catalog repeated a pagination cursor.");
|
|
2440
|
+
}
|
|
2441
|
+
cursor = nextCursor;
|
|
2442
|
+
}
|
|
2443
|
+
throw this.modelCatalogError(
|
|
2444
|
+
`Codex model catalog exceeded ${CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES} pages.`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
async readConfig(params = {}) {
|
|
2449
|
+
const client = await this.activeClient();
|
|
2450
|
+
const cwd = normalizeAgentText(params.cwd);
|
|
2451
|
+
return this.runRequest(
|
|
2452
|
+
() => client.request("config/read", {
|
|
2453
|
+
...(cwd ? { cwd } : {}),
|
|
2454
|
+
includeLayers: params.includeLayers === true
|
|
2455
|
+
}),
|
|
2456
|
+
"codex-app-server-config-read"
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
async trustProject(cwd = "") {
|
|
2461
|
+
if (!path.isAbsolute(cwd)) throw new TypeError("Codex project trust requires an absolute worktree path.");
|
|
2462
|
+
const projectRoot = path.resolve(cwd);
|
|
2463
|
+
const client = await this.activeClient();
|
|
2464
|
+
return this.runRequest(async () => {
|
|
2465
|
+
const { config } = await client.request("config/read", { cwd: projectRoot, includeLayers: false });
|
|
2466
|
+
if (config?.projects?.[projectRoot]?.trust_level === "trusted") return;
|
|
2467
|
+
// Codex discovers project hooks before applying thread config overrides.
|
|
2468
|
+
// Trust only this managed worktree through its native configuration API.
|
|
2469
|
+
await client.request("config/batchWrite", {
|
|
2470
|
+
edits: [{
|
|
2471
|
+
keyPath: `projects.${JSON.stringify(projectRoot)}.trust_level`,
|
|
2472
|
+
value: "trusted",
|
|
2473
|
+
mergeStrategy: "upsert"
|
|
2474
|
+
}],
|
|
2475
|
+
expectedVersion: null,
|
|
2476
|
+
filePath: null,
|
|
2477
|
+
reloadUserConfig: true
|
|
2478
|
+
});
|
|
2479
|
+
}, "codex-app-server-project-trust");
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
async listHooks(cwds = []) {
|
|
2483
|
+
const client = await this.activeClient();
|
|
2484
|
+
return this.runRequest(
|
|
2485
|
+
() => client.request("hooks/list", {
|
|
2486
|
+
cwds: (Array.isArray(cwds) ? cwds : [cwds])
|
|
2487
|
+
.map(normalizeAgentText)
|
|
2488
|
+
.filter(Boolean)
|
|
2489
|
+
}),
|
|
2490
|
+
"codex-app-server-hooks-list"
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
async writeHookTrustState(state = {}) {
|
|
2495
|
+
if (!isPlainObject(state) || Object.keys(state).length === 0) {
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
const client = await this.activeClient();
|
|
2499
|
+
return this.runRequest(
|
|
2500
|
+
() => client.request("config/batchWrite", {
|
|
2501
|
+
edits: [{
|
|
2502
|
+
keyPath: "hooks.state",
|
|
2503
|
+
mergeStrategy: "upsert",
|
|
2504
|
+
value: state
|
|
2505
|
+
}],
|
|
2506
|
+
expectedVersion: null,
|
|
2507
|
+
filePath: null,
|
|
2508
|
+
reloadUserConfig: true
|
|
2509
|
+
}),
|
|
2510
|
+
"codex-app-server-hook-trust-write"
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
async unsubscribeThread(threadId = "") {
|
|
2515
|
+
const client = await this.activeClient();
|
|
2516
|
+
return this.runRequest(
|
|
2517
|
+
() => client.request("thread/unsubscribe", {
|
|
2518
|
+
threadId: normalizeAgentText(threadId)
|
|
2519
|
+
}),
|
|
2520
|
+
"codex-app-server-thread-unsubscribe"
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
async deleteThread(threadId = "") {
|
|
2525
|
+
const client = await this.activeClient();
|
|
2526
|
+
return this.runRequest(
|
|
2527
|
+
() => client.request("thread/delete", {
|
|
2528
|
+
threadId: normalizeAgentText(threadId)
|
|
2529
|
+
}),
|
|
2530
|
+
"codex-app-server-thread-delete"
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
async sendTurn(threadId = "", input = [], params = {}, { signal } = {}) {
|
|
2535
|
+
const operation = async () => {
|
|
2536
|
+
const client = await this.activeClient();
|
|
2537
|
+
return client.request("turn/start", {
|
|
2538
|
+
...params,
|
|
2539
|
+
input: codexTurnInput(input),
|
|
2540
|
+
threadId: normalizeAgentText(threadId || params.threadId)
|
|
2541
|
+
}, { signal });
|
|
2542
|
+
};
|
|
2543
|
+
const response = this.options.prepareThreadEnvironment
|
|
2544
|
+
? await this.withThreadEnvironment(threadId || params.threadId, {}, operation)
|
|
2545
|
+
: await this.runRequest(operation, "codex-app-server-turn-start");
|
|
2546
|
+
return {
|
|
2547
|
+
...normalizeAgentTurn({
|
|
2548
|
+
id: response?.turn?.id || response?.turnId,
|
|
2549
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2550
|
+
raw: response?.turn || response
|
|
2551
|
+
}),
|
|
2552
|
+
response
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
async steerTurn(threadId = "", turnId = "", input = [], params = {}, { signal } = {}) {
|
|
2557
|
+
const client = await this.activeClient();
|
|
2558
|
+
const response = await this.runRequest(
|
|
2559
|
+
() => client.request("turn/steer", {
|
|
2560
|
+
...params,
|
|
2561
|
+
expectedTurnId: normalizeAgentText(turnId || params.expectedTurnId),
|
|
2562
|
+
input: codexTurnInput(input),
|
|
2563
|
+
threadId: normalizeAgentText(threadId || params.threadId)
|
|
2564
|
+
}, { signal }),
|
|
2565
|
+
"codex-app-server-turn-steer"
|
|
2566
|
+
);
|
|
2567
|
+
return {
|
|
2568
|
+
...normalizeAgentTurn({
|
|
2569
|
+
id: response?.turn?.id || response?.turnId || turnId,
|
|
2570
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
2571
|
+
raw: response?.turn || response
|
|
2572
|
+
}),
|
|
2573
|
+
response
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
async interruptTurn(threadId = "", turnId = "") {
|
|
2578
|
+
const client = await this.activeClient();
|
|
2579
|
+
return this.runRequest(
|
|
2580
|
+
() => this.#interruptTurn(client, normalizeAgentText(threadId), normalizeAgentText(turnId)),
|
|
2581
|
+
"codex-app-server-turn-interrupt"
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
async #interruptTurn(client, threadId, turnId, signal) {
|
|
2586
|
+
this.interruptedTurns.add(JSON.stringify([threadId, turnId]));
|
|
2587
|
+
const result = await client.request("turn/interrupt", { threadId, turnId }, { signal });
|
|
2588
|
+
await Promise.all([...this.commandExecutions.values()]
|
|
2589
|
+
.filter((command) => command.threadId === threadId && command.turnId === turnId)
|
|
2590
|
+
.map((command) => this.#stopCommand(command)));
|
|
2591
|
+
return result;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
#stopCommand(command) {
|
|
2595
|
+
if (command.stopTask) return command.stopTask;
|
|
2596
|
+
command.stopTask = (async () => {
|
|
2597
|
+
const signal = AbortSignal.timeout(5000);
|
|
2598
|
+
const request = (method, params) => command.client.request(method, params, { signal });
|
|
2599
|
+
while (!command.completed) {
|
|
2600
|
+
signal.throwIfAborted();
|
|
2601
|
+
let cursor;
|
|
2602
|
+
let terminal;
|
|
2603
|
+
const cursors = new Set();
|
|
2604
|
+
do {
|
|
2605
|
+
const page = await request("thread/backgroundTerminals/list", {
|
|
2606
|
+
threadId: command.threadId, limit: 100, ...(cursor ? { cursor } : {})
|
|
2607
|
+
});
|
|
2608
|
+
if (!Array.isArray(page.data)) throw new Error("Codex returned no command inventory.");
|
|
2609
|
+
terminal = page.data.find((entry) => entry.processId === command.processId);
|
|
2610
|
+
cursor = page.nextCursor;
|
|
2611
|
+
if (cursor && cursors.has(cursor)) throw new Error("Codex repeated its command inventory cursor.");
|
|
2612
|
+
cursors.add(cursor);
|
|
2613
|
+
} while (!terminal && cursor);
|
|
2614
|
+
if (command.completed) return;
|
|
2615
|
+
if (terminal) {
|
|
2616
|
+
// A recycled process ID must never target a different command.
|
|
2617
|
+
if (terminal.itemId !== command.itemId) throw new Error("The native command identity changed during Stop.");
|
|
2618
|
+
const stopped = await request("thread/backgroundTerminals/terminate", {
|
|
2619
|
+
threadId: command.threadId, processId: command.processId
|
|
2620
|
+
});
|
|
2621
|
+
if (stopped.terminated === true) return;
|
|
2622
|
+
}
|
|
2623
|
+
// Codex emits command-start before adding it to the process inventory.
|
|
2624
|
+
// Absence during that interval is not proof that the command exited.
|
|
2625
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2626
|
+
}
|
|
2627
|
+
})().catch((cause) => {
|
|
2628
|
+
const error = Object.assign(new Error("Codex could not confirm that the stopped turn's command exited.", { cause }), {
|
|
2629
|
+
code: `${this.errorPrefix}codex_command_stop_unconfirmed`
|
|
2630
|
+
});
|
|
2631
|
+
this.commandStopFailure = error;
|
|
2632
|
+
// Reuse the existing verified-runtime-stop owner when native control fails.
|
|
2633
|
+
// That owner may already be awaiting this command while stopping a lost
|
|
2634
|
+
// observation. Do not make command cleanup wait on its own caller.
|
|
2635
|
+
void Promise.resolve(this.failObservation(error)).catch(() => null);
|
|
2636
|
+
throw error;
|
|
2637
|
+
});
|
|
2638
|
+
return command.stopTask;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
nativeCliResumeCommand(threadId = "") {
|
|
2642
|
+
const runtime = this.runtime || {};
|
|
2643
|
+
return codexCliResumeCommand({
|
|
2644
|
+
codexCommand: this.options.codexCommand || "codex",
|
|
2645
|
+
endpoint: runtime.endpoint,
|
|
2646
|
+
threadId
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
close() {
|
|
2651
|
+
this.options.onClose?.();
|
|
2652
|
+
this.conversationRuntime.close();
|
|
2653
|
+
this.threadControlProbes.clear();
|
|
2654
|
+
this.commandExecutions.clear();
|
|
2655
|
+
this.interruptedTurns.clear();
|
|
2656
|
+
this.planUsage = null;
|
|
2657
|
+
this.planUsagePending = null;
|
|
2658
|
+
this.notificationSubscribers.clear();
|
|
2659
|
+
this.client?.close();
|
|
2660
|
+
this.client = null;
|
|
2661
|
+
this.initializeResult = null;
|
|
2662
|
+
this.helperAuth = null;
|
|
2663
|
+
this.helperAuthBlocked = false;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
async stopRuntime({
|
|
2667
|
+
expectedAccountIdentitySignature = "",
|
|
2668
|
+
preserveProcessExitProof = false
|
|
2669
|
+
} = {}) {
|
|
2670
|
+
this.close();
|
|
2671
|
+
this.runtimeStopOwner = this.runtime || this.runtimeStopOwner;
|
|
2672
|
+
const runtime = this.runtimeStopOwner || {};
|
|
2673
|
+
const result = await this.host.runtime.stop({
|
|
2674
|
+
...this.host.parameters,
|
|
2675
|
+
expectedAccountIdentitySignature,
|
|
2676
|
+
ownedRuntime: runtime,
|
|
2677
|
+
preserveProcessExitProof,
|
|
2678
|
+
runtimeDir: runtime.runtimeDir || this.options.runtimeDir
|
|
2679
|
+
});
|
|
2680
|
+
if (result.processExitVerified === true && this.runtimeStopOwner === runtime) {
|
|
2681
|
+
this.runtimeStopOwner = {
|
|
2682
|
+
...runtime,
|
|
2683
|
+
processExitVerifiedAt: runtime.processExitVerifiedAt || new Date().toISOString(),
|
|
2684
|
+
processState: CODEX_APP_SERVER_PROCESS_STATE.STOPPED
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
if (this.runtime === runtime) {
|
|
2688
|
+
this.runtime = null;
|
|
2689
|
+
}
|
|
2690
|
+
return result;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
export {
|
|
2695
|
+
CODEX_APP_SERVER_INVALID_REQUEST_CODE, assertCodexAuthPreflightReady,
|
|
2696
|
+
createCodexAppServerModelCatalogCache, codexAppServerProviderConnectionGeneration,
|
|
2697
|
+
ensureCodexAppServerThread, sendCodexAppServerPrompt,
|
|
2698
|
+
resumeExactCodexAppServerThread, startFreshCodexAppServerThread,
|
|
2699
|
+
defineCodexRenewalThreadIds, codexRenewalThreadError,
|
|
2700
|
+
codexAppServerEndpointForTarget, codexAppServerRequestIsInvalid, codexAppServerThreadIsMissing,
|
|
2701
|
+
codexAppServerThreadHasReadableHistory, deleteCodexAppServerThread, deleteCodexAppServerHelperThread,
|
|
2702
|
+
inspectCodexAppServerMessageAdmission,
|
|
2703
|
+
codexCliResumeCommand, codexLocalImageInput, codexTextInput, codexTurnInput, shellQuote,
|
|
2704
|
+
exportCodexNativeHistory, retireCodexConversationHistory
|
|
2705
|
+
};
|
|
2706
|
+
export {
|
|
2707
|
+
createCodexAppServerProviderOwner,
|
|
2708
|
+
codexAppServerOwnedRuntimeKey,
|
|
2709
|
+
codexAppServerRuntimeStopWasVerified
|
|
2710
|
+
} from "./codexProviderOwner.js";
|