@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.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. 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";