@jskit-ai/assistant-core 0.2.7 → 0.2.9

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -1,406 +1,20 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { lstat, realpath } from "node:fs/promises";
3
- import path from "node:path";
1
+ import { CODEX_APP_SERVER_EXECUTION_MODES, CODEX_APP_SERVER_PROCESS_STATE, CODEX_APP_SERVER_PROVIDER_ID, CODEX_APP_SERVER_TRANSPORT, codexAppServerExecutionMode, codexAppServerHelperWorkspaceDir, codexAppServerIsHelper, codexAppServerRuntimeIdentity, codexAppServerTerminalEnvHash, ensureWritablePrivateDirectory, tailTextFile } from "./codexProcess.js";
4
2
  import { CodexAppServerJsonRpcClient } from "./codexClient.js";
5
- import { releaseSupersededCodexRuntime } from "./codexProviderOwner.js";
6
- import {
7
- createCodexAccountReader, codexAuthOutputRequiresReconnect, codexAppServerProjectHookTrustConfig,
8
- codexAppServerTextHasControlCharacters, CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH
9
- } from "./codexConfiguration.js";
10
- import {
11
- CODEX_APP_SERVER_PROCESS_STATE, CODEX_APP_SERVER_TRANSPORT,
12
- codexAppServerExecutionMode, codexAppServerHelperWorkspaceDir, codexAppServerIsHelper,
13
- codexAppServerRuntimeIdentity, codexAppServerTerminalEnvHash,
14
- ensureWritablePrivateDirectory, normalizeCodexAppServerTerminalEnv, tailTextFile
15
- } from "./codexProcess.js";
16
- import { assertCodexAuthPreflightReady } from "./codexAuthPreflight.js";
3
+ import { codexAppServerTextHasControlCharacters } from "./codexConfiguration.js";
4
+ import { codexAuthOutputRequiresReconnect, createCodexAccountReader } from "./codexAccountReader.js";
17
5
  import { codexAppServerModelCatalogError } from "./codexModelCatalog.js";
18
- import { prepareCodexHistory } from "./codexHistoryAdapter.js";
19
6
  import { createCodexConversationAdapter } from "./providers/codex.js";
20
- import { exportCodexNativeHistory } from "./codexNativeHistoryExport.js";
21
- import { retireNativeConversation, readStoppedNativeDatabase } from "./nativeHistoryExport.js";
22
- import { isRecord, normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
7
+ import { codexAppServerHelperRequestError, createCodexProviderAuth } from "./codexProviderAuth.js";
8
+ import { codexAppServerThreadRequestParams, createCodexProviderThreads, keepCodexThreadParams, resumeCodexThreadAsIs } from "./codexProviderThreads.js";
9
+ import { createCodexProviderInventories } from "./codexProviderInventories.js";
10
+ import { createCodexProviderTurns } from "./codexProviderTurns.js";
11
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
23
12
  import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
24
- import {
25
- codexAppServerProviderThreadTurns,
26
- codexAppServerProviderTurnItems,
27
- codexAppServerProviderTurnClientIds,
28
- codexAppServerThreadRawValue,
29
- codexAppServerErrorText
30
- } from "./codexEvents.js";
31
-
32
- const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
33
- const CODEX_APP_SERVER_EXECUTION_MODES = Object.freeze({ HELPER: "helper", INTERACTIVE: "interactive" });
34
- const codexAppServerHelperAuthError = createCodexAccountReader().error;
13
+ import path from "node:path";
35
14
 
36
- const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
37
- const CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT = 100;
38
- const CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES = 100;
39
- const CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRIES = 1000;
40
- const CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRY_BYTES = 32 * 1024;
41
- const CODEX_APP_SERVER_MODEL_CATALOG_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
42
- const CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH = 512;
43
- const CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT = 100;
44
- const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_PAGES = 100;
45
- export const CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT = 1000;
46
- export const CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH = 512;
47
15
  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_APP_SERVER_CHATGPT_REFRESH_METHOD = "account/chatgptAuthTokens/refresh";
50
- const CODEX_NATIVE_STORAGE_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview",
51
- "subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"];
52
-
53
- function codexAppServerRequestIsInvalid(error = null, method = "") {
54
- const expectedMethod = normalizeAgentText(method);
55
- const actualMethod = normalizeAgentText(error?.method);
56
- return Number(error?.code) === CODEX_APP_SERVER_INVALID_REQUEST_CODE &&
57
- (!expectedMethod || !actualMethod || actualMethod === expectedMethod);
58
- }
59
-
60
- function codexAppServerThreadIsMissing(error = null, threadId = "") {
61
- const normalizedThreadId = normalizeAgentText(threadId || "").toLowerCase();
62
- if (
63
- !normalizedThreadId ||
64
- !codexAppServerRequestIsInvalid(error, "thread/read")
65
- ) {
66
- return false;
67
- }
68
- // Codex also uses -32600 for unrelated invalid requests, so recovery must
69
- // match both the missing-thread wording and the exact durable thread id.
70
- const message = normalizeAgentText(error?.message || "").toLowerCase();
71
- return [
72
- `thread not loaded: ${normalizedThreadId}`,
73
- `thread ${normalizedThreadId} not found`,
74
- `no rollout found for thread id ${normalizedThreadId}`
75
- ].includes(message);
76
- }
77
-
78
- async function retireCodexConversationHistory(provider, binding, {
79
- toolHomeSource, beforeDelete, signal, errorPrefix = ""
80
- } = {}) {
81
- const inspect = async () => {
82
- const ids = [...new Set([binding.conversationId, ...await provider.listThreadDescendants(binding.conversationId)])].sort();
83
- const records = [];
84
- for (const conversationId of ids) {
85
- let thread;
86
- try { thread = codexAppServerThreadRawValue(await provider.readThreadStatus(conversationId)); }
87
- catch (error) {
88
- // Native thread/read also says "not loaded" after deletion. Only
89
- // an exhaustive native inventory can distinguish absence from an
90
- // existing unloaded thread, including one moved to another cwd.
91
- if (codexAppServerThreadIsMissing(error, conversationId) &&
92
- !await provider.nativeThreadExists(conversationId, { signal })) continue;
93
- throw error;
94
- }
95
- if (thread.id !== conversationId || thread.cwd !== binding.workdir ||
96
- !["idle", "notLoaded"].includes(thread.status?.type)) {
97
- throw new Error("Codex retirement requires an idle native family in the exact saved directory.");
98
- }
99
- if (thread.historyMode !== "paginated") {
100
- throw Object.assign(new Error("Codex native retirement requires paginated history. This unsupported conversation needs operator cleanup."),
101
- { code: `${errorPrefix}codex_paginated_history_required` });
102
- }
103
- const relative = path.isAbsolute(thread.path || "") && toolHomeSource
104
- ? path.relative(path.join(toolHomeSource, ".codex"), thread.path) : "";
105
- if (thread.path && (!/^(?:sessions|archived_sessions)\//u.test(relative) || !/\.jsonl(?:\.zst)?$/u.test(thread.path))) {
106
- throw new Error("Codex returned an unsafe or unknown rollout path.");
107
- }
108
- const files = [];
109
- const plainPath = thread.path?.replace(/\.zst$/u, "");
110
- for (const file of plainPath ? [plainPath, `${plainPath}.zst`] : []) {
111
- try {
112
- const info = await lstat(file);
113
- if (!info.isFile() || info.isSymbolicLink() || await realpath(file) !== path.resolve(file)) {
114
- throw new Error("Codex rollout is not a regular file at its exact native path.");
115
- }
116
- files.push({ path: file, inode: info.ino, size: info.size, modified: info.mtimeMs });
117
- } catch (error) { if (error.code !== "ENOENT") throw error; }
118
- }
119
- records.push({ conversationId, workdir: thread.cwd, historyMode: thread.historyMode, ...(files[0] || {}), files,
120
- nativePath: thread.path ?? null, status: thread.status.type,
121
- ...(Number.isFinite(thread.createdAt) ? { createdAt: new Date(thread.createdAt * 1000).toISOString() } : {}),
122
- ...(Number.isFinite(thread.updatedAt) ? { updatedAt: new Date(thread.updatedAt * 1000).toISOString() } : {}) });
123
- }
124
- return records;
125
- };
126
- return retireNativeConversation({ binding, inspect, beforeDelete,
127
- exportConversation: (id, onRecord) => provider.exportThreadHistory(id, onRecord, { signal }),
128
- remove: () => provider.deleteThread(binding.conversationId) });
129
- }
130
-
131
- // Native Codex0.159.1 keeps goals in its separate goals_1.sqlite store.
132
- // Return its original SQL fields; the host owns stopped conversion eligibility,
133
- // including rejecting active/blocked/limited goals and resolving SQLite home.
134
- async function readCodexNativeGoal(databasePath, { threadId, signal } = {}) {
135
- if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(threadId || "") ||
136
- path.basename(databasePath || "") !== "goals_1.sqlite") {
137
- throw new TypeError("Codex goal inspection requires its exact thread ID and supported goals_1.sqlite path.");
138
- }
139
- return readStoppedNativeDatabase(databasePath, database => {
140
- const columns = database.prepare("PRAGMA table_info(thread_goals)").all();
141
- const text = ["thread_id", "goal_id", "objective", "status"];
142
- const integers = ["token_budget", "tokens_used", "time_used_seconds", "created_at_ms", "updated_at_ms"];
143
- if (!columns.some(column => column.name === "thread_id" && column.pk === 1) ||
144
- text.some(name => !columns.some(column => column.name === name && column.type === "TEXT")) ||
145
- integers.some(name => !columns.some(column => column.name === name && column.type === "INTEGER"))) {
146
- throw new Error("Codex's saved native goal schema is unsupported; no history was changed.");
147
- }
148
- const rows = database.prepare(`SELECT thread_id, goal_id, objective, status, token_budget,
149
- tokens_used, time_used_seconds, created_at_ms, updated_at_ms
150
- FROM thread_goals WHERE thread_id = ? LIMIT 2`).all(threadId);
151
- if (!rows.length) return null;
152
- const row = rows[0];
153
- if (rows.length !== 1 || row.thread_id !== threadId ||
154
- !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(row.goal_id || "") ||
155
- typeof row.objective !== "string" ||
156
- !["active", "paused", "blocked", "usage_limited", "budget_limited", "complete"].includes(row.status) ||
157
- row.token_budget !== null && (!Number.isSafeInteger(row.token_budget) || row.token_budget < 0) ||
158
- ["tokens_used", "time_used_seconds", "created_at_ms", "updated_at_ms"].some(key =>
159
- !Number.isSafeInteger(row[key]) || row[key] < 0)) {
160
- throw new Error("Codex's saved native goal is unsupported or ambiguous; no history was changed.");
161
- }
162
- return row;
163
- }, { signal });
164
- }
165
-
166
- async function codexAppServerThreadHasReadableHistory(provider = null, threadId = "") {
167
- const normalizedThreadId = normalizeAgentText(threadId);
168
- if (!normalizedThreadId || typeof provider?.readThread !== "function") {
169
- return false;
170
- }
171
- try {
172
- await provider.readThread(normalizedThreadId);
173
- return true;
174
- } catch (error) {
175
- if (codexAppServerRequestIsInvalid(error, "thread/read")) {
176
- return false;
177
- }
178
- throw error;
179
- }
180
- }
181
-
182
- // The caller has already checked the requested thread against its current
183
- // conversation. History can confirm this exact authored input, never infer it
184
- // from an assistant item or from a failed read.
185
- async function inspectCodexAppServerMessageAdmission({ provider, threadId, messageId } = {}) {
186
- try {
187
- const thread = await provider.readThread(threadId);
188
- const accepted = codexAppServerProviderThreadTurns(thread).find((turn) =>
189
- codexAppServerProviderTurnItems(turn).some((item) => item.type === "userMessage" &&
190
- codexAppServerProviderTurnClientIds({ items: [item] }).includes(messageId)));
191
- return { ok: true, admission: accepted ? "accepted" : "unknown", messageId, threadId,
192
- turnId: normalizeAgentText(accepted?.id) };
193
- } catch {
194
- return { ok: true, admission: "unknown", messageId, threadId };
195
- }
196
- }
197
16
 
198
- // Native result validation only. The caller owns any interruption, absence
199
- // proof and durable cleanup receipt.
200
- async function deleteCodexAppServerThread({ provider, threadId } = {}) {
201
- const result = await provider.deleteThread(threadId);
202
- if (!isRecord(result)) {
203
- const error = new Error("Codex app-server returned an invalid thread deletion result.");
204
- error.code = "codex_helper_thread_delete_unconfirmed";
205
- throw error;
206
- }
207
- return result;
208
- }
209
-
210
- // The caller persists cleanup ownership before this operation and removes that
211
- // receipt only after success. Native failure details remain local to that owner.
212
- async function deleteCodexAppServerHelperThread({ provider, threadId, turnId = "" } = {}) {
213
- let interruptError = null;
214
- if (turnId) {
215
- if (typeof provider?.interruptTurn === "function") {
216
- try {
217
- await provider.interruptTurn(threadId, turnId);
218
- } catch (error) {
219
- interruptError = error;
220
- }
221
- } else {
222
- interruptError = new Error("Codex provider cannot interrupt an active helper turn.");
223
- }
224
- }
225
- if (typeof provider?.deleteThread !== "function") {
226
- return { ok: false, error: new Error("Codex provider cannot delete a helper thread."), interruptError };
227
- }
228
- try {
229
- const result = await deleteCodexAppServerThread({ provider, threadId });
230
- return {
231
- deleted: true,
232
- interrupted: Boolean(turnId) && !interruptError,
233
- ok: true,
234
- result,
235
- status: "deleted",
236
- threadId,
237
- turnId,
238
- interruptError
239
- };
240
- } catch (error) {
241
- let absent = false;
242
- if (codexAppServerRequestIsInvalid(error, "thread/delete")) {
243
- try {
244
- absent = !await codexAppServerThreadHasReadableHistory(provider, threadId);
245
- } catch {
246
- absent = false;
247
- }
248
- }
249
- if (absent) {
250
- return {
251
- deleted: false,
252
- interrupted: Boolean(turnId) && !interruptError,
253
- ok: true,
254
- status: "notFound",
255
- threadId,
256
- turnId,
257
- interruptError
258
- };
259
- }
260
- return { ok: false, error, interruptError };
261
- }
262
- }
263
-
264
- function codexApplicationToolConfiguration(tools) {
265
- const dynamicTools = (tools?.schemas || []).map(({ function: tool }) => ({
266
- type: "function", name: tool.name, description: tool.description, inputSchema: tool.parameters
267
- }));
268
- return { dynamicTools, toolSchemaIdentity: createHash("sha256").update(JSON.stringify(dynamicTools)).digest("hex") };
269
- }
270
-
271
- function assertCodexApplicationToolSchemaIdentity(threadId, savedIdentity, toolSchemaIdentity) {
272
- if (threadId && (savedIdentity || codexApplicationToolConfiguration().toolSchemaIdentity) !== toolSchemaIdentity) {
273
- throw new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools.");
274
- }
275
- }
276
-
277
- /** Retained thread selection keeps observation ahead of a resume that can start work. */
278
- async function ensureCodexAppServerThread({
279
- observeThread,
280
- provider,
281
- settings,
282
- projectHooks = false,
283
- identity,
284
- applicationTools,
285
- providerReady,
286
- workdir = "",
287
- onStage = () => {}
288
- } = {}) {
289
- if (typeof observeThread !== "function") {
290
- throw new TypeError("Main Codex threads require an observer before they can resume.");
291
- }
292
- const normalizedWorkdir = normalizeAgentText(workdir);
293
- let stageStartedAt = Date.now();
294
- const availability = typeof provider.ensureAvailable === "function"
295
- ? await provider.ensureAvailable()
296
- : null;
297
- const appServerRuntime = availability?.runtime || await provider.ensureRuntime();
298
- onStage({ durationMs: Date.now() - stageStartedAt, stage: "runtime" });
299
- const existingThreadId = identity.read(normalizedWorkdir);
300
- const toolConfiguration = applicationTools ? codexApplicationToolConfiguration(applicationTools) : null;
301
- if (toolConfiguration) {
302
- if (typeof identity.readToolSchemaIdentity !== "function") throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
303
- assertCodexApplicationToolSchemaIdentity(existingThreadId, await identity.readToolSchemaIdentity(normalizedWorkdir), toolConfiguration.toolSchemaIdentity);
304
- }
305
- let config = null;
306
- if (projectHooks) {
307
- stageStartedAt = Date.now();
308
- config = await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir);
309
- onStage({ durationMs: Date.now() - stageStartedAt, stage: "hook-config" });
310
- }
311
- let { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
312
- if (toolConfiguration) {
313
- threadSettings = { ...threadSettings, dynamicTools: toolConfiguration.dynamicTools };
314
- threadStartSettings = { ...threadStartSettings, dynamicTools: toolConfiguration.dynamicTools };
315
- }
316
- if (providerReady) await providerReady({ provider, threadId: existingThreadId });
317
- let thread = null;
318
- stageStartedAt = Date.now();
319
- if (existingThreadId) {
320
- if (identity.pauseGoal) {
321
- const { goal } = await provider.readGoal(existingThreadId);
322
- if (goal?.status === "active") await provider.setGoalStatus(existingThreadId, "paused");
323
- }
324
- // Resuming can immediately start an active goal before the RPC returns.
325
- await observeThread(existingThreadId);
326
- thread = await provider.resumeThread(existingThreadId, threadSettings);
327
- if (identity.pauseGoal) {
328
- await identity.clearPause();
329
- }
330
- } else {
331
- thread = await provider.startThread(threadStartSettings);
332
- }
333
- onStage({ durationMs: Date.now() - stageStartedAt, stage: existingThreadId ? "resume" : "start" });
334
- const threadId = normalizeAgentText(thread.id || existingThreadId);
335
- if (!threadId) {
336
- throw new Error("Codex app-server did not return a thread id.");
337
- }
338
- stageStartedAt = Date.now();
339
- const recorded = await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir,
340
- ...(toolConfiguration ? { toolSchemaIdentity: toolConfiguration.toolSchemaIdentity } : {}) });
341
- // The session no longer uses the runtime directory it recorded before (an
342
- // earlier sign-in's). Releasing it is cleanup: a failure keeps the directory.
343
- const superseded = normalizeAgentText(recorded?.supersededRuntimeDir);
344
- if (superseded) await releaseSupersededCodexRuntime(superseded).catch(() => null);
345
- if (providerReady) await providerReady({ provider, threadId });
346
- onStage({ durationMs: Date.now() - stageStartedAt, stage: "identity-metadata" });
347
- return {
348
- appServerRuntime,
349
- thread,
350
- threadId
351
- };
352
- }
353
-
354
- function codexAppServerHelperLoginParams(auth = {}, errorPrefix = "") {
355
- if (auth.authMode === "chatgpt") {
356
- return {
357
- accessToken: auth.accessToken,
358
- chatgptAccountId: auth.accountId,
359
- chatgptPlanType: auth.planType || null,
360
- type: "chatgptAuthTokens"
361
- };
362
- }
363
- if (auth.authMode === "apiKey") {
364
- return {
365
- apiKey: auth.apiKey,
366
- type: "apiKey"
367
- };
368
- }
369
- throw codexAppServerHelperAuthError(
370
- `${errorPrefix}codex_helper_auth_invalid`,
371
- "The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
372
- );
373
- }
374
-
375
- function codexAppServerHelperLoginResponseType(auth = {}) {
376
- return auth.authMode === "chatgpt" ? "chatgptAuthTokens" : "apiKey";
377
- }
378
-
379
- function codexAppServerHelperAccountType(auth = {}) {
380
- return auth.authMode === "chatgpt" ? "chatgpt" : "apiKey";
381
- }
382
-
383
- function codexAppServerHelperRequestError(error = null, reason = "", errorPrefix = "") {
384
- if (
385
- error?.name === "AbortError" ||
386
- error?.code === "ABORT_ERR" ||
387
- (typeof error?.code === "string" && error.code.startsWith(`${errorPrefix}codex_helper_`)) ||
388
- error?.code === `${errorPrefix}codex_model_catalog_invalid`
389
- ) {
390
- return error;
391
- }
392
- const failure = new Error("Codex isolated helper execution failed. Retry the task.");
393
- failure.code = `${errorPrefix}codex_helper_provider_request_failed`;
394
- const providerCode = Number(error?.code);
395
- if (Number.isSafeInteger(providerCode)) {
396
- failure.providerCode = providerCode;
397
- }
398
- const normalizedReason = normalizeAgentText(reason);
399
- if (normalizedReason && normalizedReason.length <= 128) {
400
- failure.operation = normalizedReason;
401
- }
402
- return failure;
403
- }
17
+ const CODEX_APP_SERVER_SERVER_INFO_USER_AGENT_MAX_LENGTH = 512;
404
18
 
405
19
  function normalizeCodexAppServerInfo(initializeResult = null) {
406
20
  if (!isRecord(initializeResult)) {
@@ -420,305 +34,39 @@ function normalizeCodexAppServerInfo(initializeResult = null) {
420
34
  });
421
35
  }
422
36
 
423
- function codexAppServerEndpointForTarget(endpoint = "") {
424
- const normalizedEndpoint = normalizeAgentText(endpoint);
425
- if (!normalizedEndpoint) {
426
- return "";
427
- }
428
- return normalizedEndpoint;
429
- }
430
-
431
- function codexAppServerThreadRequestParams(params = {}, threadEnv = {}) {
432
- const source = isRecord(params) ? params : {};
433
- const config = isRecord(source.config) ? source.config : {};
434
- if (Object.hasOwn(config, "shell_environment_policy")) {
435
- return source;
436
- }
437
- return {
438
- ...source,
439
- config: {
440
- ...config,
441
- shell_environment_policy: {
442
- inherit: "none",
443
- set: normalizeCodexAppServerTerminalEnv(threadEnv)
444
- }
445
- }
446
- };
447
- }
448
-
449
- function codexTextInput(text = "") {
450
- return {
451
- text: String(text ?? ""),
452
- text_elements: [],
453
- type: "text"
454
- };
455
- }
456
-
457
- function codexLocalImageInput(attachments = []) {
458
- return attachments
459
- .filter((attachment) => attachment.contentType?.startsWith("image/"))
460
- .map((attachment) => ({ type: "localImage", path: attachment.path }));
461
- }
462
-
463
- async function sendCodexAppServerPrompt({
464
- attachments = [],
465
- clientUserMessageId = "",
466
- outputSchema = null,
467
- provider,
468
- prompt = "",
469
- threadId = "",
470
- readOnly = false
471
- } = {}, authorized = {}) {
472
- const authoredInput = String(prompt ?? "");
473
- const preparedInput = authorized.preparedInput;
474
- if (preparedInput === undefined && !authoredInput.trim()) {
475
- throw new Error("Codex app-server prompt is empty.");
476
- }
477
- const input = preparedInput === undefined ? [authoredInput, ...codexLocalImageInput(attachments)] : preparedInput;
478
- const turnSettings = {
479
- ...authorized.turnSettings,
480
- ...(readOnly
481
- ? {
482
- sandboxPolicy: {
483
- networkAccess: false,
484
- type: "readOnly"
485
- }
486
- }
487
- : {}),
488
- ...(normalizeAgentText(clientUserMessageId)
489
- ? { clientUserMessageId: normalizeAgentText(clientUserMessageId) }
490
- : {})
491
- };
492
- if (outputSchema && typeof outputSchema === "object" && !Array.isArray(outputSchema)) {
493
- turnSettings.outputSchema = outputSchema;
494
- }
495
- const turn = await provider.sendTurn(threadId, input, turnSettings);
496
- return {
497
- input,
498
- turn
499
- };
500
- }
501
-
502
- function codexTurnInput(input = []) {
503
- const values = Array.isArray(input) ? input : [input];
504
- return values.map((item) => {
505
- if (isRecord(item) && item.type === "localImage" && typeof item.path === "string") {
506
- return { type: "localImage", path: item.path };
507
- }
508
- if (isRecord(item) && item.type === "image" && typeof item.url === "string") {
509
- return { type: "image", url: item.url };
510
- }
511
- if (isRecord(item) && item.type === "text") {
512
- return codexTextInput(item.text);
513
- }
514
- return codexTextInput(item);
515
- });
516
- }
517
-
518
- function shellQuote(value = "") {
519
- const text = String(value ?? "");
520
- if (/^[A-Za-z0-9_./:=@+-]+$/u.test(text)) {
521
- return text;
522
- }
523
- return `'${text.replaceAll("'", "'\"'\"'")}'`;
524
- }
525
-
526
- function codexControlProbeCommand(keys, marker) {
527
- const source = [
528
- "const {createHash}=require('node:crypto');",
529
- `const keys=${JSON.stringify(keys)};`,
530
- `process.stdout.write(${JSON.stringify(marker)}+createHash('sha256').update(JSON.stringify(keys.map(key=>[key,process.env[key]]))).digest('hex'))`
531
- ].join("");
532
- return `${shellQuote(process.execPath)} -e ${shellQuote(source)}`;
533
- }
37
+ const CODEX_APP_SERVER_CHATGPT_REFRESH_METHOD = "account/chatgptAuthTokens/refresh";
534
38
 
535
- function codexCliResumeCommand({
536
- codexCommand = "",
537
- endpoint = "",
538
- threadId = ""
539
- } = {}) {
540
- const normalizedEndpoint = codexAppServerEndpointForTarget(endpoint);
541
- const resolvedCodexCommand = normalizeAgentText(codexCommand) || "codex";
542
- const normalizedThreadId = normalizeAgentText(threadId);
543
- if (!normalizedEndpoint) {
544
- throw new Error("Codex app-server endpoint is required for the native CLI command.");
545
- }
546
- if (!normalizedThreadId) {
547
- throw new Error("Codex thread id is required for the native CLI command.");
548
- }
549
- const argv = [
550
- resolvedCodexCommand,
551
- "-c",
552
- "check_for_update_on_startup=false",
553
- "--remote",
554
- normalizedEndpoint,
555
- "resume",
556
- normalizedThreadId
557
- ];
558
- return {
559
- argv,
560
- command: argv.map(shellQuote).join(" ")
561
- };
562
- }
39
+ // Option callbacks an application leaves out.
40
+ function readNoInstructions() {}
563
41
 
564
- function codexAppServerThreadInventoryError(errorCode, label, detail) {
565
- const error = new Error(`Codex ${label} thread inventory ${detail}.`);
566
- error.code = errorCode;
567
- return error;
42
+ function keepThreadEnvironment(environment) {
43
+ return environment;
568
44
  }
569
45
 
570
- // Why a listed inventory entry is refused, or "" for a valid thread.
571
- function codexAppServerThreadInventoryRefusal(entry, threadId, { cwd, verifyCwd }) {
572
- if (!isRecord(entry)) return "listed an entry that is not a thread";
573
- if (!threadId) return "listed a thread without an id";
574
- if (threadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH) {
575
- return `listed a thread id longer than ${CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH} characters`;
576
- }
577
- if (codexAppServerTextHasControlCharacters(threadId)) return "listed a thread id with control characters";
578
- if (verifyCwd && normalizeAgentText(entry.cwd) !== cwd) return `listed thread ${threadId} outside the requested directory`;
579
- return "";
580
- }
46
+ function prepareNoThreadResume() {}
581
47
 
582
- async function listBoundedCodexAppServerThreadIds({
583
- ancestorThreadId = "",
584
- archived = false,
585
- client,
586
- cwd = "",
587
- errorCode = "",
588
- findThreadId = "",
589
- label = "",
590
- requestLabel = "",
591
- runRequest,
592
- signal = null,
593
- sourceKinds = ["appServer"],
594
- state,
595
- verifyCwd = false
596
- } = {}) {
597
- const seenCursors = new Set();
598
- let cursor = "";
599
- for (let page = 0; page < CODEX_APP_SERVER_THREAD_INVENTORY_MAX_PAGES; page += 1) {
600
- if (cursor && seenCursors.has(cursor)) {
601
- throw codexAppServerThreadInventoryError(
602
- errorCode,
603
- label,
604
- "repeated a pagination cursor"
605
- );
606
- }
607
- if (cursor) {
608
- seenCursors.add(cursor);
609
- }
610
- const response = await runRequest(
611
- () => client.request("thread/list", {
612
- archived,
613
- ...(cursor ? { cursor } : {}),
614
- ...(ancestorThreadId ? { ancestorThreadId } : cwd ? { cwd } : {}),
615
- limit: CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT,
616
- modelProviders: [],
617
- sourceKinds,
618
- useStateDbOnly: false
619
- }, { signal }),
620
- requestLabel
621
- );
622
- if (!Array.isArray(response?.data) || response.data.length > CODEX_APP_SERVER_THREAD_INVENTORY_PAGE_LIMIT) {
623
- throw codexAppServerThreadInventoryError(errorCode, label, "returned a malformed page");
624
- }
625
- // Only thread ids are retained, so only they are bounded. A search for one
626
- // thread retains none and is bounded by the page limit alone.
627
- if (!findThreadId && state.entryCount + response.data.length > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT) {
628
- throw codexAppServerThreadInventoryError(errorCode, label,
629
- `exceeded its ${CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT}-thread limit`);
630
- }
631
- for (const entry of response.data) {
632
- const threadId = isRecord(entry) ? normalizeAgentText(entry.id) : "";
633
- const refusal = codexAppServerThreadInventoryRefusal(entry, threadId, { cwd, verifyCwd });
634
- if (refusal) {
635
- throw codexAppServerThreadInventoryError(errorCode, label, refusal);
636
- }
637
- if (findThreadId) {
638
- if (threadId === findThreadId) {
639
- state.found = true;
640
- return state;
641
- }
642
- continue;
643
- }
644
- state.entryCount += 1;
645
- state.threadIds.add(threadId);
646
- }
647
- const nextCursor = response.nextCursor === undefined || response.nextCursor === null || response.nextCursor === ""
648
- ? ""
649
- : typeof response.nextCursor === "string"
650
- ? response.nextCursor.trim()
651
- : null;
652
- if (
653
- nextCursor === null ||
654
- nextCursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH ||
655
- (nextCursor && seenCursors.has(nextCursor))
656
- ) {
657
- throw codexAppServerThreadInventoryError(
658
- errorCode,
659
- label,
660
- "returned an invalid pagination cursor"
661
- );
662
- }
663
- if (!nextCursor) {
664
- return state;
665
- }
666
- cursor = nextCursor;
667
- }
668
- throw codexAppServerThreadInventoryError(
669
- errorCode,
670
- label,
671
- "exceeded its page limit"
672
- );
673
- }
674
-
675
- function codexPlanUsage(limits = {}) {
676
- if (limits?.limitId && limits.limitId !== "codex") return { status: "unavailable", windows: [], checkedAt: Date.now() };
677
- const windows = ["primary", "secondary"].flatMap((id) => {
678
- const value = limits?.[id];
679
- if (typeof value?.usedPercent !== "number" || !Number.isFinite(value.usedPercent) || value.usedPercent < 0) return [];
680
- return [{
681
- id,
682
- remainingPercent: Math.max(0, 100 - Math.min(100, value.usedPercent)),
683
- windowDurationMins: Number.isInteger(value.windowDurationMins) && value.windowDurationMins > 0 ? value.windowDurationMins : null,
684
- resetsAt: Number.isSafeInteger(value.resetsAt) && value.resetsAt > 0 ? value.resetsAt : null
685
- }];
686
- });
687
- return { status: windows.length ? "available" : "unavailable", windows, checkedAt: Date.now() };
688
- }
48
+ function logNothing() {}
689
49
 
690
- function normalizeAgentThread(value = {}) {
691
- const thread = value && typeof value === "object" && !Array.isArray(value) ? value : {};
692
- return {
693
- id: normalizeAgentText(thread.id || thread.threadId),
694
- provider: normalizeAgentText(thread.provider),
695
- raw: thread.raw || thread
696
- };
697
- }
50
+ function reportNoObservationLoss() {}
698
51
 
699
- function normalizeAgentTurn(value = {}) {
700
- const turn = value && typeof value === "object" && !Array.isArray(value) ? value : {};
701
- return {
702
- id: normalizeAgentText(turn.id || turn.turnId),
703
- provider: normalizeAgentText(turn.provider),
704
- raw: turn.raw || turn,
705
- status: normalizeAgentText(turn.status || turn.raw?.status)
706
- };
707
- }
52
+ function closeNothing() {}
708
53
 
709
- // The one reading of a sendTurn delivery. Its turn is already normalized; the
710
- // provider's own error stays on the raw native turn.
711
- function codexAppServerDeliveredTurn(delivery = {}) {
712
- const turn = delivery?.turn || {};
713
- return {
714
- turnId: normalizeAgentText(turn.id),
715
- status: normalizeAgentText(turn.status || turn.raw?.status),
716
- error: codexAppServerErrorText(turn.raw?.error || turn.response?.turn?.error || turn.error)
717
- };
54
+ function runDirectly(operation) {
55
+ return operation();
718
56
  }
719
57
 
58
+ /**
59
+ * One account's connection to its native Codex app-server: the runtime it ensures, the client it opens, the
60
+ * notifications it publishes and the server requests it routes. Its auth, turns, threads and inventories are
61
+ * composed from their own modules; their operations are this provider's members.
62
+ */
720
63
  export class CodexAppServerAgentProvider {
721
- constructor(options = {}, host = {}) {
64
+ /**
65
+ * @param {object} options The provider options: execution mode, account selection and the application's
66
+ * thread callbacks.
67
+ * @param {object} host The native host: credentials, runtime, launch parameters, execution and configuration.
68
+ */
69
+ constructor(options, host) {
722
70
  this.host = host;
723
71
  this.credentials = host.credentials;
724
72
  this.configuration = {
@@ -732,9 +80,6 @@ export class CodexAppServerAgentProvider {
732
80
  this.errorPrefix = this.configuration.errorPrefix;
733
81
  this.accountReader = createCodexAccountReader({ errorPrefix: this.errorPrefix, ...this.configuration.accountReader });
734
82
  this.modelCatalogError = (message) => codexAppServerModelCatalogError(message, this.errorPrefix);
735
- this.planUsage = null;
736
- this.planUsagePending = null;
737
- this.planUsageAuthGeneration = 0;
738
83
  this.availabilityPromise = null;
739
84
  this.stale = false;
740
85
  this.options = options;
@@ -743,48 +88,93 @@ export class CodexAppServerAgentProvider {
743
88
  this.connectionGeneration = 0;
744
89
  this.notificationSubscribers = new Set();
745
90
  this.settingsWaiters = new Set();
746
- this.helperAuth = null;
747
- this.helperAuthBlocked = false;
748
91
  this.initializeResult = null;
749
92
  this.runtime = null;
750
93
  this.runtimeStopOwner = null;
751
94
  this.runtimePromise = null;
752
95
  this.serverRequestHandler = null;
753
96
  this.threadRequestHandlers = new Map();
97
+
98
+ const auth = createCodexProviderAuth({ provider: this });
99
+ const turns = createCodexProviderTurns({ provider: this });
100
+ const threads = createCodexProviderThreads({ provider: this });
101
+ const inventories = createCodexProviderInventories({ provider: this });
102
+ this.auth = auth;
103
+ this.turns = turns;
104
+
105
+ const {
106
+ readInstructions = readNoInstructions,
107
+ prepareThreadEnvironment = keepThreadEnvironment,
108
+ prepareThreadParams = keepCodexThreadParams,
109
+ prepareThreadResumeParams = prepareNoThreadResume,
110
+ beforeResumeThread = resumeCodexThreadAsIs
111
+ } = options;
112
+ const { log = logNothing } = host;
754
113
  this.conversationRuntime = createCodexConversationAdapter({
755
- readInstructions: (params, threadId) => this.options.readInstructions?.(params, threadId),
114
+ readInstructions: (params, threadId) => readInstructions(params, threadId),
756
115
  client: () => this.activeClient(),
757
116
  runtime: () => this.runtime,
758
117
  runRequest: (run, reason) => this.runRequest(run, reason),
759
118
  timeoutMs: options.threadControlTimeoutMs,
760
119
  environment: options.threadEnv,
761
- prepareEnvironment: (environment) => this.options.prepareThreadEnvironment?.(environment) || environment,
762
- prepareParameters: (params) => this.options.prepareThreadParams?.(params),
763
- prepareResume: (id, params, context) => this.options.prepareThreadResumeParams?.(id, params, context),
764
- beforeResume: (id) => this.options.beforeResumeThread?.(id),
120
+ prepareEnvironment: (environment) => prepareThreadEnvironment(environment) || environment,
121
+ prepareParameters: (params) => prepareThreadParams(params),
122
+ prepareResume: (id, params, context) => prepareThreadResumeParams(id, params, context),
123
+ beforeResume: (id) => beforeResumeThread(id),
765
124
  prepareHistory: (params, client, context) => this.withHistoryAdapter(params, client, context),
766
125
  threadParameters: codexAppServerThreadRequestParams,
767
- verifyEnvironment: (...args) => this.#verifyThreadEnvironment(...args),
768
- recordInterruption: (threadId, turnId) => {
769
- const key = JSON.stringify([threadId, turnId]);
770
- this.interruptedTurns.set(key, this.interruptedTurns.get(key) || "control_reconfiguration");
771
- },
772
- onRecoveryFailure: (id) => {
773
- const probe = this.threadControlProbes.get(id);
774
- if (probe && !probe.verified) {
775
- this.threadControlProbes.delete(id);
776
- for (const pending of probe.pending) this.publishNotification(pending);
777
- }
778
- },
126
+ verifyEnvironment: turns.verifyThreadEnvironment,
127
+ recordInterruption: turns.recordInterruption,
128
+ onRecoveryFailure: turns.abandonControlProbe,
779
129
  publishGoal: (threadId, goal) => this.publishNotification({ method: "thread/goal/updated", params: { threadId, goal } }),
780
- log: (event, fields, context) => host.log?.(event, fields, {
781
- ...context, connectionGeneration: this.connectionGeneration
782
- })
130
+ log: (event, fields, context) => log(event, fields, { ...context, connectionGeneration: this.connectionGeneration })
783
131
  });
784
- this.threadControlProbes = new Map();
785
- this.commandExecutions = new Map();
786
- this.interruptedTurns = new Map();
787
- this.commandStopFailure = null;
132
+
133
+ this.selectedHelperAuth = auth.selectedHelperAuth;
134
+ this.assertHelperAccountIdentityCurrent = auth.assertHelperAccountIdentityCurrent;
135
+ this.authenticateHelperClient = auth.authenticateHelperClient;
136
+ this.refreshHelperChatgptAuth = auth.refreshHelperChatgptAuth;
137
+ this.preflightAuth = auth.preflightAuth;
138
+ this.assertRuntimeAuthReady = auth.assertRuntimeAuthReady;
139
+ this.markReconnectRequired = auth.markReconnectRequired;
140
+ this.readPlanUsage = auth.readPlanUsage;
141
+
142
+ this.sendTurn = turns.sendTurn;
143
+ this.steerTurn = turns.steerTurn;
144
+ this.interruptionOutcome = turns.interruptionOutcome;
145
+ this.interruptTurn = turns.interruptTurn;
146
+ this.nativeCliResumeCommand = turns.nativeCliResumeCommand;
147
+ this.isControlProbeTurn = turns.isControlProbeTurn;
148
+ this.stopThreadForObservationLoss = turns.stopThreadForObservationLoss;
149
+
150
+ this.startThread = threads.startThread;
151
+ this.resumeThread = threads.resumeThread;
152
+ this.withHistoryAdapter = threads.withHistoryAdapter;
153
+ this.withThreadEnvironment = threads.withThreadEnvironment;
154
+ this.ensureThreadControls = threads.ensureThreadControls;
155
+ this.readGoal = threads.readGoal;
156
+ this.updateThreadSettings = threads.updateThreadSettings;
157
+ this.setGoal = threads.setGoal;
158
+ this.setGoalStatus = threads.setGoalStatus;
159
+ this.clearGoal = threads.clearGoal;
160
+ this.readThread = threads.readThread;
161
+ this.readThreadStatus = threads.readThreadStatus;
162
+ this.listThreadTurns = threads.listThreadTurns;
163
+ this.exportThreadHistory = threads.exportThreadHistory;
164
+ this.unsubscribeThread = threads.unsubscribeThread;
165
+ this.deleteThread = threads.deleteThread;
166
+
167
+ this.listLoadedThreads = inventories.listLoadedThreads;
168
+ this.listAppServerThreadsForCwd = inventories.listAppServerThreadsForCwd;
169
+ this.listThreadDescendants = inventories.listThreadDescendants;
170
+ this.listNativeThreadsForCwd = inventories.listNativeThreadsForCwd;
171
+ this.nativeThreadExists = inventories.nativeThreadExists;
172
+ this.listHelperThreads = inventories.listHelperThreads;
173
+ this.listModels = inventories.listModels;
174
+ this.readConfig = inventories.readConfig;
175
+ this.trustProject = inventories.trustProject;
176
+ this.listHooks = inventories.listHooks;
177
+ this.writeHookTrustState = inventories.writeHookTrustState;
788
178
  }
789
179
 
790
180
  get threadEnvironments() { return this.conversationRuntime.threadEnvironments; }
@@ -795,123 +185,25 @@ export class CodexAppServerAgentProvider {
795
185
  return codexAppServerIsHelper(this.options);
796
186
  }
797
187
 
798
- async selectedHelperAuth() {
799
- if (!this.isHelperProvider()) {
800
- return null;
801
- }
802
- if (this.host.parameters.providerHome) {
803
- return { identitySignature: await this.credentials.identity(this.host.parameters) };
804
- }
805
- return this.accountReader.readSelected(this.options);
188
+ currentConnectionGeneration() {
189
+ return this.connectionGeneration;
806
190
  }
807
191
 
808
- async assertHelperAccountIdentityCurrent() {
809
- if (!this.isHelperProvider() || !this.helperAuth) {
810
- return;
811
- }
812
- if (this.helperAuthBlocked) {
813
- throw this.accountReader.error(
814
- `${this.errorPrefix}codex_helper_auth_changed`,
815
- "The selected Codex account changed during helper work. Retry the task with the current account."
816
- );
817
- }
818
- const current = await this.selectedHelperAuth();
819
- if (current.identitySignature !== this.helperAuth.identitySignature) {
820
- this.helperAuthBlocked = true;
821
- throw this.accountReader.error(
822
- `${this.errorPrefix}codex_helper_auth_changed`,
823
- "The selected Codex account changed during helper work. Retry the task with the current account."
824
- );
825
- }
192
+ currentServerInfo() {
193
+ return this.initializeResult;
826
194
  }
827
195
 
828
- async authenticateHelperClient(client, runtime = {}) {
829
- if (!this.isHelperProvider()) {
830
- return null;
831
- }
832
- const auth = await this.selectedHelperAuth();
833
- if (
834
- !runtime.accountIdentitySignature ||
835
- auth.identitySignature !== runtime.accountIdentitySignature
836
- ) {
837
- throw this.accountReader.error(
838
- `${this.errorPrefix}codex_helper_auth_changed`,
839
- "The selected Codex account changed while helper execution was starting. Retry the task."
840
- );
841
- }
842
- this.helperAuth = auth;
843
- this.helperAuthBlocked = false;
844
- if (this.host.parameters.providerHome) return auth;
845
- try {
846
- const login = await client.request(
847
- "account/login/start",
848
- codexAppServerHelperLoginParams(auth, this.errorPrefix)
849
- );
850
- if (normalizeAgentText(login?.type) !== codexAppServerHelperLoginResponseType(auth)) {
851
- throw new Error("invalid login response");
852
- }
853
- const account = await client.request("account/read", {
854
- refreshToken: false
855
- });
856
- if (
857
- account?.requiresOpenaiAuth !== true ||
858
- normalizeAgentText(account?.account?.type) !== codexAppServerHelperAccountType(auth)
859
- ) {
860
- throw new Error("invalid account response");
861
- }
862
- return auth;
863
- } catch {
864
- this.helperAuth = null;
865
- this.helperAuthBlocked = true;
866
- throw this.accountReader.error(
867
- `${this.errorPrefix}codex_helper_auth_unavailable`,
868
- "Codex could not activate the selected account for isolated helper work. Reconnect Codex and retry."
869
- );
870
- }
196
+ isAvailable() {
197
+ return Boolean(this.client?.isOpen() && this.runtime);
871
198
  }
872
199
 
873
- async refreshHelperChatgptAuth(params = {}) {
874
- const currentAuth = this.helperAuth;
875
- const previousAccountId = params.previousAccountId;
876
- if (
877
- !this.isHelperProvider() ||
878
- currentAuth?.authMode !== "chatgpt" ||
879
- params.reason !== "unauthorized" ||
880
- typeof previousAccountId !== "string" ||
881
- previousAccountId.length === 0 ||
882
- previousAccountId.length > CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH ||
883
- previousAccountId !== currentAuth.accountId
884
- ) {
885
- this.helperAuthBlocked = true;
886
- throw this.accountReader.error(
887
- `${this.errorPrefix}codex_helper_auth_refresh_rejected`,
888
- "Codex helper authentication refresh was rejected. Reconnect Codex and retry."
889
- );
890
- }
891
- const refreshed = await this.selectedHelperAuth();
892
- if (
893
- refreshed.authMode !== "chatgpt" ||
894
- refreshed.identitySignature !== currentAuth.identitySignature
895
- ) {
896
- this.helperAuthBlocked = true;
897
- throw this.accountReader.error(
898
- `${this.errorPrefix}codex_helper_auth_changed`,
899
- "The selected Codex account changed during helper work. Retry the task with the current account."
900
- );
901
- }
902
- if (refreshed.secretSignature === currentAuth.secretSignature) {
903
- this.helperAuthBlocked = true;
904
- throw this.accountReader.error(
905
- `${this.errorPrefix}codex_helper_auth_refresh_pending`,
906
- "The selected Codex account has not produced a newer access token yet. Reconnect Codex and retry."
907
- );
200
+ subscribe(callback) {
201
+ if (typeof callback !== "function") {
202
+ throw new TypeError("Codex app-server observer must be a function.");
908
203
  }
909
- this.helperAuth = refreshed;
910
- return {
911
- accessToken: refreshed.accessToken,
912
- chatgptAccountId: refreshed.accountId,
913
- chatgptPlanType: refreshed.planType || null
914
- };
204
+ // Observation belongs to this provider, not one replaceable socket.
205
+ this.notificationSubscribers.add(callback);
206
+ return () => this.notificationSubscribers.delete(callback);
915
207
  }
916
208
 
917
209
  async handleServerRequest(request = {}, sourceClient = this.client) {
@@ -935,7 +227,7 @@ export class CodexAppServerAgentProvider {
935
227
  }
936
228
  return threadHandler.handler(request);
937
229
  }
938
- if (typeof this.serverRequestHandler === "function") {
230
+ if (this.serverRequestHandler) {
939
231
  return this.serverRequestHandler(request);
940
232
  }
941
233
  const error = new Error("Codex app-server request is not supported by this client.");
@@ -958,10 +250,12 @@ export class CodexAppServerAgentProvider {
958
250
  }
959
251
  }
960
252
 
961
- // An account change leaves a runtime that is still working on the
962
- // credentials it started with. Codex reads them once, so this provider
963
- // finishes its running turn on them and then stops; it never re-checks,
964
- // replaces or restarts its runtime, and never marks the new account invalid.
253
+ /**
254
+ * An account change leaves a runtime that is still working on the
255
+ * credentials it started with. Codex reads them once, so this provider
256
+ * finishes its running turn on them and then stops; it never re-checks,
257
+ * replaces or restarts its runtime, and never marks the new account invalid.
258
+ */
965
259
  markStale() {
966
260
  this.stale = true;
967
261
  }
@@ -992,63 +286,6 @@ export class CodexAppServerAgentProvider {
992
286
  return this.runtime;
993
287
  }
994
288
 
995
- async preflightAuth(reason = "codex-auth-preflight", modelProviderId = "") {
996
- if (await this.options.prepareAuth?.(modelProviderId) === "external") {
997
- await this.assertRuntimeAuthReady(reason);
998
- return;
999
- }
1000
- if (this.options.modelProviderId && this.options.modelProviderId !== "openai") {
1001
- await this.credentials.assertCurrent("");
1002
- return;
1003
- }
1004
- await this.assertRuntimeAuthReady(reason);
1005
- try {
1006
- return await assertCodexAuthPreflightReady({ ...this.options, codexCommand: this.host.parameters.codexCommand }, {
1007
- execution: this.host.execution, credentials: this.credentials, reason
1008
- });
1009
- } catch (error) {
1010
- if (error?.code === this.credentials.reconnectRequiredCode && this.runtime?.runtimeDir) {
1011
- await this.stopRuntimeAndRequireDrain(reason);
1012
- }
1013
- throw error;
1014
- }
1015
- }
1016
-
1017
- async assertRuntimeAuthReady(reason = "codex-app-server") {
1018
- if (this.isHelperProvider() || this.stale) {
1019
- return;
1020
- }
1021
- const runtime = this.runtime || {};
1022
- let generationError = null;
1023
- try {
1024
- await this.credentials.assertCurrent(runtime.authStateSignature);
1025
- } catch (error) {
1026
- generationError = error;
1027
- }
1028
- const logTail = await tailTextFile(runtime.logPath || "");
1029
- const reconnectRequired = codexAuthOutputRequiresReconnect(logTail);
1030
- if (!generationError && !reconnectRequired) {
1031
- return;
1032
- }
1033
- await this.stopRuntimeAndRequireDrain(reason);
1034
- if (reconnectRequired) {
1035
- await this.markReconnectRequired({
1036
- toolHomeSource: runtime.toolHomeSource || this.options.toolHomeSource,
1037
- observed: logTail,
1038
- reason
1039
- });
1040
- throw this.credentials.reconnectError({
1041
- observed: logTail
1042
- });
1043
- }
1044
- throw generationError;
1045
- }
1046
-
1047
- async markReconnectRequired({ reason = "codex-app-server", observed = "", toolHomeSource } = {}) {
1048
- await this.credentials.markInvalid({ reason, toolHomeSource });
1049
- throw this.credentials.reconnectError({ modelProviderId: this.options.modelProviderId, observed });
1050
- }
1051
-
1052
289
  async runRequest(operation, reason = "codex-app-server-request") {
1053
290
  try {
1054
291
  await this.assertHelperAccountIdentityCurrent();
@@ -1103,7 +340,7 @@ export class CodexAppServerAgentProvider {
1103
340
 
1104
341
  async connect() {
1105
342
  if (this.observationFailure) throw this.observationFailure;
1106
- if (this.client?.isOpen?.() && this.runtime) {
343
+ if (this.client?.isOpen() && this.runtime) {
1107
344
  return {
1108
345
  initializeResult: null,
1109
346
  reusedClient: true,
@@ -1125,7 +362,7 @@ export class CodexAppServerAgentProvider {
1125
362
  }
1126
363
 
1127
364
  async openConnection() {
1128
- if (this.client?.isOpen?.() && this.runtime) {
365
+ if (this.client?.isOpen() && this.runtime) {
1129
366
  return {
1130
367
  initializeResult: null,
1131
368
  reusedClient: true,
@@ -1133,14 +370,14 @@ export class CodexAppServerAgentProvider {
1133
370
  };
1134
371
  }
1135
372
  const runtime = await this.ensureRuntime();
1136
- if (this.client?.isOpen?.()) {
373
+ if (this.client?.isOpen()) {
1137
374
  return {
1138
375
  initializeResult: null,
1139
376
  reusedClient: true,
1140
377
  runtime
1141
378
  };
1142
379
  }
1143
- this.client?.close?.();
380
+ this.client?.close();
1144
381
  this.client = null;
1145
382
  this.initializeResult = null;
1146
383
  const client = new CodexAppServerJsonRpcClient({
@@ -1186,10 +423,7 @@ export class CodexAppServerAgentProvider {
1186
423
  }
1187
424
  : runtime;
1188
425
  this.runtime = null;
1189
- if (helper) {
1190
- this.helperAuth = null;
1191
- this.helperAuthBlocked = true;
1192
- }
426
+ if (helper) this.auth.blockHelper();
1193
427
  if (cleanup.cleanupFailed) {
1194
428
  const failure = new Error(
1195
429
  helper
@@ -1207,18 +441,10 @@ export class CodexAppServerAgentProvider {
1207
441
  throw error;
1208
442
  }
1209
443
  this.client = client;
1210
- this.planUsage = null;
1211
- this.planUsagePending = null;
444
+ this.auth.resetPlanUsage();
1212
445
  client.subscribe((notification = {}) => {
1213
446
  if (this.client !== client || this.observationFailure) return;
1214
- if (notification.method === "account/updated") {
1215
- this.planUsage = null;
1216
- this.planUsageAuthGeneration += 1;
1217
- }
1218
- if (notification.method === "account/rateLimits/updated") {
1219
- const limits = notification.params?.rateLimits;
1220
- if (!limits?.limitId || limits.limitId === "codex") this.planUsage = codexPlanUsage(limits);
1221
- }
447
+ this.auth.observeAccountNotification(notification);
1222
448
  this.publishNotification(notification);
1223
449
  });
1224
450
  this.initializeResult = normalizeCodexAppServerInfo(initializeResult);
@@ -1229,68 +455,12 @@ export class CodexAppServerAgentProvider {
1229
455
  };
1230
456
  }
1231
457
 
1232
- currentConnectionGeneration() {
1233
- return this.connectionGeneration;
1234
- }
1235
-
1236
458
  publishNotification(notification) {
1237
- const threadId = notification.params?.threadId;
1238
459
  if (notification.method === "thread/goal/updated") {
1239
460
  notification = { ...notification, params: { ...notification.params,
1240
- goal: this.conversationRuntime.projectGoal(threadId, notification.params.goal) } };
1241
- }
1242
- const turnId = notification.params?.turnId;
1243
- const item = notification.params?.item;
1244
- if (item?.type === "commandExecution" && item.processId && item.source === "unifiedExecStartup") {
1245
- const key = JSON.stringify([threadId, turnId, item.id]);
1246
- if (notification.method === "item/completed") {
1247
- const command = this.commandExecutions.get(key);
1248
- if (command) command.completed = true;
1249
- this.commandExecutions.delete(key);
1250
- } else if (notification.method === "item/started") {
1251
- const command = this.commandExecutions.get(key) || {
1252
- threadId, turnId, itemId: item.id, processId: item.processId, client: this.client
1253
- };
1254
- this.commandExecutions.set(key, command);
1255
- // Native startup may finish after turn/interrupt has already returned.
1256
- if (this.interruptedTurns.has(JSON.stringify([threadId, turnId]))) {
1257
- void this.#stopCommand(command).catch(() => null);
1258
- }
1259
- }
1260
- }
1261
- const probe = this.threadControlProbes.get(threadId);
1262
- const method = notification.method;
1263
- if (probe && (/^(turn|item)\//u.test(method) || method === "thread/status/changed")) {
1264
- const turnId = notification.params?.turnId || notification.params?.turn?.id;
1265
- if (method === "turn/started" && probe.turnId && probe.turnId !== turnId) {
1266
- this.threadControlProbes.delete(threadId);
1267
- } else if (probe.verified) {
1268
- if (turnId === probe.turnId) {
1269
- if (method === "item/completed" && notification.params.item?.id === probe.itemId) {
1270
- probe.result = notification.params.item;
1271
- }
1272
- if (method === "turn/completed") probe.completed = true;
1273
- }
1274
- return;
1275
- } else {
1276
- if (method === "turn/started") probe.turnId = turnId;
1277
- probe.pending.push(notification);
1278
- if (method === "item/started" || probe.pending.length >= 32) {
1279
- const item = notification.params?.item;
1280
- if (item?.type === "commandExecution" && item.command?.includes(probe.marker)) {
1281
- probe.verified = true;
1282
- probe.itemId = item.id;
1283
- probe.pending = [];
1284
- } else {
1285
- // A concurrent native user turn must retain every event. Only a
1286
- // positively identified control probe may be hidden from chat.
1287
- this.threadControlProbes.delete(threadId);
1288
- for (const pending of probe.pending) this.publishNotification(pending);
1289
- }
1290
- }
1291
- return;
1292
- }
461
+ goal: this.conversationRuntime.projectGoal(notification.params?.threadId, notification.params.goal) } };
1293
462
  }
463
+ if (!this.turns.observeNotification(notification)) return;
1294
464
  for (const subscriber of this.notificationSubscribers) {
1295
465
  try {
1296
466
  subscriber(notification);
@@ -1300,15 +470,6 @@ export class CodexAppServerAgentProvider {
1300
470
  }
1301
471
  }
1302
472
 
1303
- currentServerInfo() {
1304
- return this.initializeResult;
1305
- }
1306
-
1307
- isControlProbeTurn(threadId, turnId) {
1308
- const probe = this.threadControlProbes.get(threadId);
1309
- return Boolean(turnId && probe?.verified && probe.turnId === turnId);
1310
- }
1311
-
1312
473
  async currentRuntimeInfo() {
1313
474
  const runtime = this.runtime || {};
1314
475
  const executionMode = codexAppServerExecutionMode(this.options);
@@ -1358,13 +519,9 @@ export class CodexAppServerAgentProvider {
1358
519
  });
1359
520
  }
1360
521
 
1361
- isAvailable() {
1362
- return Boolean(this.client?.isOpen?.() && this.runtime);
1363
- }
1364
-
1365
522
  async activeClient() {
1366
523
  if (this.observationFailure) throw this.observationFailure;
1367
- if (this.client?.isOpen?.()) {
524
+ if (this.client?.isOpen()) {
1368
525
  return this.client;
1369
526
  }
1370
527
  await this.connect();
@@ -1406,45 +563,6 @@ export class CodexAppServerAgentProvider {
1406
563
  }
1407
564
  }
1408
565
 
1409
- async readPlanUsage() {
1410
- const client = this.client;
1411
- if (!client?.isOpen?.()) return { status: "unavailable", windows: [] };
1412
- if (this.planUsage && Date.now() - this.planUsage.checkedAt < 60_000) return this.planUsage;
1413
- if (this.planUsagePending) return this.planUsagePending;
1414
- const previous = this.planUsage;
1415
- const authGeneration = this.planUsageAuthGeneration;
1416
- const signal = AbortSignal.timeout(10_000);
1417
- const operation = (async () => {
1418
- try {
1419
- const account = await client.request("account/read", { refreshToken: false }, { signal });
1420
- if (this.client !== client || this.planUsageAuthGeneration !== authGeneration) return { status: "unavailable", windows: [] };
1421
- if (account?.account?.type !== "chatgpt") {
1422
- this.planUsage = { status: "unsupported", windows: [], checkedAt: Date.now() };
1423
- } else {
1424
- const result = await client.request("account/rateLimits/read", {}, { signal });
1425
- if (this.client !== client || this.planUsageAuthGeneration !== authGeneration) return { status: "unavailable", windows: [] };
1426
- // A live event received during the read is newer than this request.
1427
- if (this.planUsage === previous) this.planUsage = codexPlanUsage(result.rateLimitsByLimitId?.codex || result.rateLimits);
1428
- }
1429
- return this.planUsage;
1430
- } catch {
1431
- return { status: "unavailable", windows: [] };
1432
- }
1433
- })();
1434
- this.planUsagePending = operation;
1435
- try { return await operation; }
1436
- finally { if (this.planUsagePending === operation) this.planUsagePending = null; }
1437
- }
1438
-
1439
- subscribe(callback) {
1440
- if (typeof callback !== "function") {
1441
- throw new TypeError("Codex app-server observer must be a function.");
1442
- }
1443
- // Observation belongs to this provider, not one replaceable socket.
1444
- this.notificationSubscribers.add(callback);
1445
- return () => this.notificationSubscribers.delete(callback);
1446
- }
1447
-
1448
566
  failObservation(cause) {
1449
567
  const message = cause?.code === `${this.errorPrefix}agent_control_recovery_failed`
1450
568
  ? cause.message
@@ -1453,51 +571,20 @@ export class CodexAppServerAgentProvider {
1453
571
  new Error(message, { cause }),
1454
572
  { code: `${this.errorPrefix}codex_observation_lost` }
1455
573
  );
1456
- return this.options.onObservationLost?.(this.observationFailure);
1457
- }
1458
-
1459
- async stopThreadForObservationLoss(threadId, turnId) {
1460
- // Control uses only this existing socket. It must not spawn or resume work
1461
- // to find out whether the work we lost sight of has stopped.
1462
- const client = this.client;
1463
- if (!client?.isOpen()) throw new Error("Codex control connection is unavailable.");
1464
- if (this.commandStopFailure) throw this.commandStopFailure;
1465
- const signal = AbortSignal.timeout(5_000);
1466
- const request = (method, params) => client.request(method, params, { signal });
1467
- let { goal } = await request("thread/goal/get", { threadId });
1468
- if (goal?.status === "active") {
1469
- const result = await request("thread/goal/set", { threadId, status: "paused" });
1470
- if (result.goal?.status !== "paused" || result.goal?.createdAt !== goal.createdAt || result.goal?.objective !== goal.objective) {
1471
- throw new Error("Codex did not confirm that the same goal was paused.");
1472
- }
1473
- goal = result.goal;
1474
- }
1475
- const readStatus = async () => {
1476
- const { thread } = await request("thread/read", { threadId, includeTurns: false });
1477
- return typeof thread?.status === "string" ? thread.status : thread?.status?.type;
1478
- };
1479
- if (!["idle", "notLoaded"].includes(await readStatus())) {
1480
- if (!turnId) {
1481
- const page = await request("thread/turns/list", { threadId, limit: 1, sortDirection: "desc", itemsView: "summary" });
1482
- turnId = page.data?.[0]?.id;
1483
- }
1484
- if (!turnId) throw new Error("Codex's running turn could not be identified.");
1485
- await this.#interruptTurn(client, threadId, turnId, signal);
1486
- if (await readStatus() !== "idle") throw new Error("Codex did not confirm that its turn stopped.");
1487
- }
1488
- return { goal };
574
+ const { onObservationLost = reportNoObservationLoss } = this.options;
575
+ return onObservationLost(this.observationFailure);
1489
576
  }
1490
577
 
1491
578
  setServerRequestHandler(callback) {
1492
579
  const handler = typeof callback === "function" ? callback : null;
1493
580
  this.serverRequestHandler = handler;
1494
581
  const client = this.client;
1495
- client?.setRequestHandler?.((request) => this.handleServerRequest(request, client));
582
+ client?.setRequestHandler((request) => this.handleServerRequest(request, client));
1496
583
  return () => {
1497
584
  if (this.serverRequestHandler === handler) {
1498
585
  this.serverRequestHandler = null;
1499
586
  const client = this.client;
1500
- client?.setRequestHandler?.((request) => this.handleServerRequest(request, client));
587
+ client?.setRequestHandler((request) => this.handleServerRequest(request, client));
1501
588
  }
1502
589
  };
1503
590
  }
@@ -1507,12 +594,11 @@ export class CodexAppServerAgentProvider {
1507
594
  throw new TypeError("Codex tool dispatch requires its exact thread and handler.");
1508
595
  }
1509
596
  if (this.threadRequestHandlers.has(threadId)) throw new Error("This Codex thread already has an application tool owner.");
1510
- const runInContext = this.options.runInContext;
1511
- const registration = { handler: typeof runInContext === "function"
1512
- ? request => runInContext(() => handler(request)) : handler };
597
+ const { runInContext = runDirectly } = this.options;
598
+ const registration = { handler: request => runInContext(() => handler(request)) };
1513
599
  this.threadRequestHandlers.set(threadId, registration);
1514
600
  const client = this.client;
1515
- client?.setRequestHandler?.(request => this.handleServerRequest(request, client));
601
+ client?.setRequestHandler(request => this.handleServerRequest(request, client));
1516
602
  return Object.freeze({
1517
603
  isCurrent: () => this.threadRequestHandlers.get(threadId) === registration,
1518
604
  release: () => {
@@ -1521,756 +607,26 @@ export class CodexAppServerAgentProvider {
1521
607
  });
1522
608
  }
1523
609
 
1524
- async startThread({ hostContext, ...params } = {}) {
1525
- params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext });
1526
- delete params.hostContext;
1527
- params = await this.options.prepareThreadParams?.(params) || params;
1528
- const client = await this.activeClient();
1529
- params = await this.withHistoryAdapter(params, client);
1530
- const environment = this.options.prepareThreadEnvironment
1531
- ? await this.options.prepareThreadEnvironment(this.options.threadEnv || {})
1532
- : this.options.threadEnv;
1533
- const requestParams = this.conversationRuntime.threadParameters(params, environment);
1534
- if (params.ephemeral !== true) requestParams.historyMode = "paginated";
1535
- const response = await this.runRequest(
1536
- () => client.request("thread/start", requestParams),
1537
- "codex-app-server-thread-start"
1538
- );
1539
- if (hostContext) {
1540
- await this.options.bindThreadContext(response?.thread?.id, hostContext, params);
1541
- }
1542
- // Native ephemeral helpers have no stored history and report "legacy".
1543
- // Paginated history is required only for persistent conversations.
1544
- if (params.ephemeral !== true && response?.thread?.historyMode === "legacy") {
1545
- throw Object.assign(new Error("Codex did not create the required paginated conversation history. Update Codex before starting a conversation."), {
1546
- code: `${this.errorPrefix}codex_history_unsupported`
1547
- });
1548
- }
1549
- if (response?.thread?.historyMode === "paginated" && response.thread.ephemeral !== true) {
1550
- const threadId = normalizeAgentText(response.thread.id);
1551
- // A new paginated thread needs its metadata and empty rollout persisted
1552
- // before it can be resumed. This initializes only the just-created,
1553
- // empty thread; it never hydrates an existing conversation.
1554
- await this.runRequest(async () => {
1555
- await client.request("thread/name/set", { threadId, name: threadId });
1556
- await client.request("thread/read", { threadId, includeTurns: true });
1557
- }, "codex-app-server-thread-initialize-history");
1558
- }
1559
- if (this.options.prepareThreadEnvironment && response?.thread?.id) {
1560
- this.conversationRuntime.registerThread(response.thread.id, {
1561
- client, executionId: normalizeAgentText(this.runtime?.executionId),
1562
- params: requestParams, managed: !Object.hasOwn(params.config || {}, "shell_environment_policy")
1563
- });
1564
- }
1565
- return {
1566
- ...normalizeAgentThread({
1567
- id: response?.thread?.id,
1568
- provider: CODEX_APP_SERVER_PROVIDER_ID,
1569
- raw: response?.thread
1570
- }),
1571
- response
1572
- };
1573
- }
1574
-
1575
- async resumeThread(threadId = "", { hostContext, ...params } = {}) {
1576
- if (hostContext) await this.options.bindThreadContext(threadId, hostContext, params);
1577
- params = await this.conversationRuntime.prepareInstructions({ ...params, hostContext }, threadId);
1578
- delete params.hostContext;
1579
- params = await this.options.prepareThreadParams?.(params) || params;
1580
- await this.options.beforeResumeThread?.(threadId);
1581
- const client = await this.activeClient();
1582
- let response;
1583
- if (this.options.prepareThreadEnvironment) {
1584
- response = await this.withThreadEnvironment(threadId, params, (requestParams, resumed) =>
1585
- resumed || client.request("thread/resume", { excludeTurns: true, ...requestParams, threadId }));
1586
- } else {
1587
- params = await this.withHistoryAdapter(params, client, { threadId });
1588
- const requestParams = codexAppServerThreadRequestParams(params, this.options.threadEnv);
1589
- response = await this.runRequest(
1590
- () => client.request("thread/resume", {
1591
- excludeTurns: true,
1592
- ...requestParams,
1593
- threadId: normalizeAgentText(threadId || requestParams.threadId)
1594
- }),
1595
- "codex-app-server-thread-resume"
1596
- );
1597
- }
1598
- return {
1599
- ...normalizeAgentThread({
1600
- id: response?.thread?.id,
1601
- provider: CODEX_APP_SERVER_PROVIDER_ID,
1602
- raw: response?.thread
1603
- }),
1604
- response
1605
- };
1606
- }
1607
-
1608
- async withHistoryAdapter(params, client, options = {}) {
1609
- return prepareCodexHistory(params, client, { ...options,
1610
- baseUrl: this.runtime?.historyAdapterBaseUrl, modelProviderId: this.options.modelProviderId });
1611
- }
1612
-
1613
- async withThreadEnvironment(threadId, params, operation) {
1614
- try {
1615
- return await this.conversationRuntime.withThreadContext(threadId, params, operation);
1616
- } catch (error) {
1617
- if (typeof error.code === "string" && error.code.startsWith("assistant_")) error.code = `${this.errorPrefix}${error.code.slice(10)}`;
1618
- throw error;
1619
- }
1620
- }
1621
-
1622
- async #verifyThreadEnvironment(threadId, environment, request, signal, { requiredKeys = [] } = {}) {
1623
- const keys = Object.keys(environment).filter((key) => key.startsWith(this.configuration.controlEnvironmentPrefix) || requiredKeys.includes(key)).sort();
1624
- if (!keys.length) return;
1625
-
1626
- const marker = `${this.configuration.controlProbePrefix}${randomUUID()}:`;
1627
- const probe = { marker, pending: [], verified: false, turnId: "" };
1628
- this.threadControlProbes.set(threadId, probe);
1629
- const expected = createHash("sha256").update(JSON.stringify(keys.map((key) => [key, environment[key]]))).digest("hex");
1630
- // This bounded native shell check uses no model and prints only a digest,
1631
- // never credentials or environment values.
1632
- await request("thread/shellCommand", {
1633
- threadId, command: codexControlProbeCommand(keys, marker), timeoutMs: 2000
1634
- });
1635
- while (true) {
1636
- signal.throwIfAborted();
1637
- // The native completion event owns this result; saved history is not a
1638
- // second acknowledgement of this live control operation.
1639
- if (probe.completed && probe.result) {
1640
- if (probe.result.type !== "commandExecution" || probe.result.exitCode !== 0 ||
1641
- probe.result.aggregatedOutput?.trim() !== `${marker}${expected}`) {
1642
- throw Object.assign(new Error("Another native subscriber retained the previous assistant settings. Close the native assistant terminal and retry Resume."), {
1643
- code: "assistant_agent_control_binding_retained"
1644
- });
1645
- }
1646
- return;
1647
- }
1648
- await new Promise((resolve) => setTimeout(resolve, 25));
1649
- }
1650
- }
1651
-
1652
- async ensureThreadControls(threadId) {
1653
- if (!this.options.prepareThreadEnvironment) return;
1654
- return this.withThreadEnvironment(threadId, {}, async (_params, resumed) => ({ ok: true, recovered: Boolean(resumed) }));
1655
- }
1656
-
1657
- async readGoal(threadId = "", { signal } = {}) {
1658
- const client = await this.activeClient();
1659
- const result = await this.runRequest(
1660
- () => client.request("thread/goal/get", { threadId: normalizeAgentText(threadId) }, { signal }),
1661
- "codex-app-server-goal-read"
1662
- );
1663
- return { ...result, goal: this.conversationRuntime.projectGoal(threadId, result.goal) };
1664
- }
1665
-
1666
- async updateThreadSettings(threadId, { model, effort } = {}, { signal } = {}) {
1667
- if (!normalizeAgentText(threadId) || !normalizeAgentText(model)) {
1668
- throw new TypeError("Codex settings require an existing thread and a model.");
1669
- }
1670
- const client = await this.activeClient();
1671
- threadId = normalizeAgentText(threadId);
1672
- const requested = { model, effort, ...(effort ? { config: { model_reasoning_effort: effort } } : {}) };
1673
- const params = await this.options.prepareThreadParams?.(requested) || requested;
1674
- const update = async selected => {
1675
- const current = await this.activeClient();
1676
- const applied = Promise.withResolvers();
1677
- // A successful RPC only queues the update. Observe its applied snapshot
1678
- // before resuming a goal or accepting a dependent settings operation.
1679
- const deadline = AbortSignal.any([AbortSignal.timeout(this.options.requestTimeoutMs || 10_000), ...(signal ? [signal] : [])]);
1680
- const abort = () => applied.reject(deadline.reason);
1681
- const unsubscribe = current.subscribe(notification => {
1682
- const settings = notification.params?.threadSettings;
1683
- if (notification.method === "thread/settings/updated" && notification.params?.threadId === threadId &&
1684
- settings?.model === selected.model && (!selected.effort || settings.effort === selected.effort)) applied.resolve();
1685
- });
1686
- this.settingsWaiters.add(applied.reject);
1687
- deadline.addEventListener("abort", abort, { once: true });
1688
- void applied.promise.catch(() => {});
1689
- try {
1690
- deadline.throwIfAborted();
1691
- const { thread } = await this.runRequest(() => current.request("thread/read", {
1692
- threadId, includeTurns: false
1693
- }, { signal: deadline }), "codex-app-server-settings-read");
1694
- // Native no-op updates may emit no settings event. A current snapshot
1695
- // also confirms values already applied by the provider reload.
1696
- if (thread?.model === selected.model && (!selected.effort || thread.reasoningEffort === selected.effort) &&
1697
- (!selected.modelProvider || thread.modelProvider === selected.modelProvider)) return {};
1698
- const result = await this.runRequest(() => current.request("thread/settings/update", {
1699
- threadId, model: selected.model, ...(selected.effort ? { effort: selected.effort } : {})
1700
- }, { signal: deadline }), "codex-app-server-settings-update");
1701
- await applied.promise;
1702
- return result;
1703
- } finally {
1704
- unsubscribe();
1705
- deadline.removeEventListener("abort", abort);
1706
- this.settingsWaiters.delete(applied.reject);
1707
- }
1708
- };
1709
- if (params.modelProvider || this.conversationRuntime.hasPendingSettings(threadId)) {
1710
- const { thread } = await client.request("thread/read", { threadId, includeTurns: false }, { signal });
1711
- if (params.modelProvider !== thread?.modelProvider || this.conversationRuntime.hasPendingSettings(threadId)) {
1712
- return this.conversationRuntime.scheduleSettings(threadId, params, update);
1713
- }
1714
- }
1715
- return update(params);
1716
- }
1717
-
1718
- async setGoal(threadId, { objective, tokenBudget } = {}, { signal } = {}) {
1719
- if (typeof objective !== "string" || !objective.trim() ||
1720
- (tokenBudget !== undefined && (!Number.isSafeInteger(tokenBudget) || tokenBudget <= 0))) {
1721
- throw new TypeError("A Codex goal requires an objective and an optional positive token budget.");
1722
- }
1723
- this.conversationRuntime.goalChanged(threadId);
1724
- const operation = async () => {
1725
- const client = await this.activeClient();
1726
- return client.request("thread/goal/set", {
1727
- threadId: normalizeAgentText(threadId), objective: objective.trim(), status: "active",
1728
- ...(tokenBudget === undefined ? {} : { tokenBudget })
1729
- }, { signal });
1730
- };
1731
- return this.options.prepareThreadEnvironment
1732
- ? this.withThreadEnvironment(threadId, {}, operation)
1733
- : this.runRequest(operation, "codex-app-server-goal-set");
1734
- }
1735
-
1736
- async setGoalStatus(threadId = "", status = "", { signal } = {}) {
1737
- if (!["active", "paused"].includes(status)) {
1738
- throw new TypeError("Invalid Codex goal status.");
1739
- }
1740
- this.conversationRuntime.goalChanged(threadId);
1741
- const operation = async () => {
1742
- const client = await this.activeClient();
1743
- return client.request("thread/goal/set", { threadId: normalizeAgentText(threadId), status }, { signal });
1744
- };
1745
- return status === "active" && this.options.prepareThreadEnvironment
1746
- ? this.withThreadEnvironment(threadId, {}, operation)
1747
- : this.runRequest(operation, "codex-app-server-goal-status");
1748
- }
1749
-
1750
- async clearGoal(threadId = "", { signal } = {}) {
1751
- this.conversationRuntime.goalChanged(threadId);
1752
- const client = await this.activeClient();
1753
- return this.runRequest(
1754
- () => client.request("thread/goal/clear", { threadId: normalizeAgentText(threadId) }, { signal }),
1755
- "codex-app-server-goal-clear"
1756
- );
1757
- }
1758
-
1759
- async readThread(threadId = "") {
1760
- const client = await this.activeClient();
1761
- const response = await this.runRequest(
1762
- () => client.request("thread/read", {
1763
- includeTurns: true,
1764
- threadId: normalizeAgentText(threadId)
1765
- }),
1766
- "codex-app-server-thread-read"
1767
- );
1768
- return {
1769
- ...normalizeAgentThread({
1770
- id: response?.thread?.id || threadId,
1771
- provider: CODEX_APP_SERVER_PROVIDER_ID,
1772
- raw: response?.thread || response
1773
- }),
1774
- response
1775
- };
1776
- }
1777
-
1778
- async readThreadStatus(threadId = "") {
1779
- const client = await this.activeClient();
1780
- const response = await this.runRequest(
1781
- () => client.request("thread/read", {
1782
- includeTurns: false,
1783
- threadId: normalizeAgentText(threadId)
1784
- }),
1785
- "codex-app-server-thread-status"
1786
- );
1787
- return {
1788
- ...normalizeAgentThread({
1789
- id: response?.thread?.id || threadId,
1790
- provider: CODEX_APP_SERVER_PROVIDER_ID,
1791
- raw: response?.thread || response
1792
- }),
1793
- response
1794
- };
1795
- }
1796
-
1797
- async listThreadTurns(threadId = "", params = {}) {
1798
- const client = await this.activeClient();
1799
- return this.runRequest(
1800
- () => client.request("thread/turns/list", {
1801
- ...params,
1802
- threadId: normalizeAgentText(threadId || params.threadId)
1803
- }),
1804
- "codex-app-server-thread-turns-list"
1805
- );
1806
- }
1807
-
1808
- async exportThreadHistory(threadId, onRecord, { signal } = {}) {
1809
- const active = await this.activeClient();
1810
- // Oversized native items must fail preservation without disconnecting the
1811
- // shared conversation observer. Reuse JSKIT's transport on a separate socket.
1812
- const client = new CodexAppServerJsonRpcClient({
1813
- endpoint: active.endpoint,
1814
- maxMessageBytes: 64 * 1024 ** 2,
1815
- requestTimeoutMs: 30_000,
1816
- WebSocketImpl: this.options.WebSocketImpl
1817
- });
1818
- const deadline = AbortSignal.timeout(300_000);
1819
- const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
1820
- boundedSignal.throwIfAborted();
1821
- const abort = () => client.close();
1822
- boundedSignal.addEventListener("abort", abort, { once: true });
1823
- try {
1824
- await client.connect();
1825
- boundedSignal.throwIfAborted();
1826
- await client.initialize({ clientInfo: this.configuration.historyClientInfo });
1827
- boundedSignal.throwIfAborted();
1828
- return await exportCodexNativeHistory(client, normalizeAgentText(threadId), onRecord, { signal: boundedSignal, errorPrefix: this.errorPrefix });
1829
- } finally {
1830
- boundedSignal.removeEventListener("abort", abort);
1831
- client.close();
1832
- }
1833
- }
1834
-
1835
- async listLoadedThreads(params = {}) {
1836
- const client = await this.activeClient();
1837
- return this.runRequest(
1838
- () => client.request("thread/loaded/list", params),
1839
- "codex-app-server-thread-loaded-list"
1840
- );
1841
- }
1842
-
1843
- async listAppServerThreadsForCwd({
1844
- cwd = "",
1845
- signal = null
1846
- } = {}) {
1847
- const normalizedCwd = normalizeAgentText(cwd);
1848
- if (!normalizedCwd || codexAppServerTextHasControlCharacters(normalizedCwd)) {
1849
- const error = new Error("Codex session thread inventory requires an exact cwd.");
1850
- error.code = `${this.errorPrefix}codex_session_thread_inventory_invalid`;
1851
- throw error;
1852
- }
1853
- const client = await this.activeClient();
1854
- const state = await listBoundedCodexAppServerThreadIds({
1855
- archived: false,
1856
- client,
1857
- cwd: normalizedCwd,
1858
- errorCode: `${this.errorPrefix}codex_session_thread_inventory_invalid`,
1859
- label: "session",
1860
- requestLabel: "codex-app-server-session-thread-list",
1861
- runRequest: this.runRequest.bind(this),
1862
- signal,
1863
- state: {
1864
- entryCount: 0,
1865
- threadIds: new Set()
1866
- },
1867
- verifyCwd: true
1868
- });
1869
- return Object.freeze({
1870
- cwd: normalizedCwd,
1871
- threadIds: Object.freeze([...state.threadIds].sort())
1872
- });
1873
- }
1874
-
1875
- // Native thread/delete cascades through all spawned descendants, including
1876
- // archived and non-app-server threads. Never infer its scope from cwd alone.
1877
- async listThreadDescendants(threadId) {
1878
- const ancestorThreadId = normalizeAgentText(threadId);
1879
- if (!ancestorThreadId || ancestorThreadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
1880
- codexAppServerTextHasControlCharacters(ancestorThreadId)) {
1881
- throw new TypeError("Codex descendant inventory requires an exact thread id.");
1882
- }
1883
- const client = await this.activeClient();
1884
- const state = { entryCount: 0, threadIds: new Set() };
1885
- for (const archived of [false, true]) {
1886
- await listBoundedCodexAppServerThreadIds({
1887
- ancestorThreadId, archived, client, state,
1888
- sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
1889
- errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "retirement",
1890
- requestLabel: "codex-app-server-descendant-list", runRequest: this.runRequest.bind(this)
1891
- });
1892
- }
1893
- return [...state.threadIds].sort();
1894
- }
1895
-
1896
- async listNativeThreadsForCwd(cwd) {
1897
- if (!path.isAbsolute(cwd || "") || codexAppServerTextHasControlCharacters(cwd)) throw new TypeError("Native inventory requires an exact absolute directory.");
1898
- const client = await this.activeClient();
1899
- const state = { entryCount: 0, threadIds: new Set() };
1900
- for (const archived of [false, true]) {
1901
- await listBoundedCodexAppServerThreadIds({ archived, client, cwd, state, verifyCwd: true,
1902
- sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
1903
- errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "native storage",
1904
- requestLabel: "codex-app-server-native-storage-list", runRequest: this.runRequest.bind(this) });
1905
- }
1906
- return [...state.threadIds].sort();
1907
- }
1908
-
1909
- async nativeThreadExists(threadId, { signal } = {}) {
1910
- const id = normalizeAgentText(threadId);
1911
- if (!id || id.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH || codexAppServerTextHasControlCharacters(id)) {
1912
- throw new TypeError("Native existence checks require an exact thread id.");
1913
- }
1914
- const client = await this.activeClient();
1915
- const state = { entryCount: 0, found: false, threadIds: new Set() };
1916
- for (const archived of [false, true]) {
1917
- await listBoundedCodexAppServerThreadIds({ archived, client, findThreadId: id, state, signal,
1918
- sourceKinds: CODEX_NATIVE_STORAGE_SOURCE_KINDS,
1919
- errorCode: `${this.errorPrefix}codex_retirement_inventory_invalid`, label: "native storage",
1920
- requestLabel: "codex-app-server-native-existence", runRequest: this.runRequest.bind(this) });
1921
- if (state.found) return true;
1922
- }
1923
- return false;
1924
- }
1925
-
1926
- async listHelperThreads({
1927
- signal = null
1928
- } = {}) {
1929
- const client = await this.activeClient();
1930
- const execution = await this.currentHelperExecutionContext();
1931
- const state = {
1932
- entryCount: 0,
1933
- threadIds: new Set()
1934
- };
1935
- for (const archived of [false, true]) {
1936
- await listBoundedCodexAppServerThreadIds({
1937
- archived,
1938
- client,
1939
- cwd: execution.cwd,
1940
- errorCode: `${this.errorPrefix}codex_helper_thread_inventory_invalid`,
1941
- label: "helper",
1942
- requestLabel: "codex-app-server-helper-thread-list",
1943
- runRequest: this.runRequest.bind(this),
1944
- signal,
1945
- state
1946
- });
1947
- }
1948
- return Object.freeze({
1949
- threadIds: Object.freeze([...state.threadIds].sort())
1950
- });
1951
- }
1952
-
1953
- async listModels(params = {}, {
1954
- signal = null
1955
- } = {}) {
1956
- const client = await this.activeClient();
1957
- const data = [];
1958
- const seenCursors = new Set();
1959
- const includeHidden = params.includeHidden === true;
1960
- const limit = normalizePositiveInteger(
1961
- params.limit,
1962
- { fallback: CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT }
1963
- );
1964
- const boundedLimit = Math.min(limit, CODEX_APP_SERVER_MODEL_CATALOG_PAGE_LIMIT);
1965
- let totalBytes = 0;
1966
- let cursor = params.cursor === undefined || params.cursor === null || params.cursor === ""
1967
- ? ""
1968
- : typeof params.cursor === "string"
1969
- ? params.cursor.trim()
1970
- : null;
1971
- if (cursor === null || cursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH) {
1972
- throw this.modelCatalogError("Codex model catalog cursor is invalid.");
1973
- }
1974
- for (let page = 0; page < CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES; page += 1) {
1975
- if (cursor && seenCursors.has(cursor)) {
1976
- throw this.modelCatalogError("Codex model catalog repeated a pagination cursor.");
1977
- }
1978
- if (cursor) {
1979
- seenCursors.add(cursor);
1980
- }
1981
- const response = await this.runRequest(
1982
- () => client.request("model/list", {
1983
- ...(cursor ? { cursor } : {}),
1984
- includeHidden,
1985
- limit: boundedLimit
1986
- }, { signal }),
1987
- "codex-app-server-model-list"
1988
- );
1989
- if (!Array.isArray(response?.data)) {
1990
- throw this.modelCatalogError(
1991
- "Codex model catalog response did not contain a data array."
1992
- );
1993
- }
1994
- if (
1995
- response.data.length > boundedLimit ||
1996
- data.length + response.data.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRIES
1997
- ) {
1998
- throw this.modelCatalogError("Codex model catalog exceeded its entry limit.");
1999
- }
2000
- for (const entry of response.data) {
2001
- if (!isRecord(entry)) {
2002
- throw this.modelCatalogError("Codex model catalog contained an invalid entry.");
2003
- }
2004
- let entryBytes = 0;
2005
- try {
2006
- entryBytes = Buffer.byteLength(JSON.stringify(entry), "utf8");
2007
- } catch {
2008
- throw this.modelCatalogError("Codex model catalog contained an invalid entry.");
2009
- }
2010
- if (
2011
- entryBytes > CODEX_APP_SERVER_MODEL_CATALOG_MAX_ENTRY_BYTES ||
2012
- totalBytes + entryBytes > CODEX_APP_SERVER_MODEL_CATALOG_MAX_TOTAL_BYTES
2013
- ) {
2014
- throw this.modelCatalogError("Codex model catalog exceeded its response limit.");
2015
- }
2016
- totalBytes += entryBytes;
2017
- data.push(entry);
2018
- }
2019
- const nextCursor = response.nextCursor === undefined || response.nextCursor === null || response.nextCursor === ""
2020
- ? ""
2021
- : typeof response.nextCursor === "string"
2022
- ? response.nextCursor.trim()
2023
- : null;
2024
- if (nextCursor === null || nextCursor.length > CODEX_APP_SERVER_MODEL_CATALOG_MAX_CURSOR_LENGTH) {
2025
- throw this.modelCatalogError("Codex model catalog cursor is invalid.");
2026
- }
2027
- if (!nextCursor) {
2028
- return {
2029
- data,
2030
- nextCursor: null
2031
- };
2032
- }
2033
- if (seenCursors.has(nextCursor)) {
2034
- throw this.modelCatalogError("Codex model catalog repeated a pagination cursor.");
2035
- }
2036
- cursor = nextCursor;
2037
- }
2038
- throw this.modelCatalogError(
2039
- `Codex model catalog exceeded ${CODEX_APP_SERVER_MODEL_CATALOG_MAX_PAGES} pages.`
2040
- );
2041
- }
2042
-
2043
- async readConfig(params = {}) {
2044
- const client = await this.activeClient();
2045
- const cwd = normalizeAgentText(params.cwd);
2046
- return this.runRequest(
2047
- () => client.request("config/read", {
2048
- ...(cwd ? { cwd } : {}),
2049
- includeLayers: params.includeLayers === true
2050
- }),
2051
- "codex-app-server-config-read"
2052
- );
2053
- }
2054
-
2055
- async trustProject(cwd = "") {
2056
- if (!path.isAbsolute(cwd)) throw new TypeError("Codex project trust requires an absolute worktree path.");
2057
- const projectRoot = path.resolve(cwd);
2058
- const client = await this.activeClient();
2059
- return this.runRequest(async () => {
2060
- const { config } = await client.request("config/read", { cwd: projectRoot, includeLayers: false });
2061
- if (config?.projects?.[projectRoot]?.trust_level === "trusted") return;
2062
- // Codex discovers project hooks before applying thread config overrides.
2063
- // Trust only this managed worktree through its native configuration API.
2064
- await client.request("config/batchWrite", {
2065
- edits: [{
2066
- keyPath: `projects.${JSON.stringify(projectRoot)}.trust_level`,
2067
- value: "trusted",
2068
- mergeStrategy: "upsert"
2069
- }],
2070
- expectedVersion: null,
2071
- filePath: null,
2072
- reloadUserConfig: true
2073
- });
2074
- }, "codex-app-server-project-trust");
2075
- }
2076
-
2077
- async listHooks(cwds = []) {
2078
- const client = await this.activeClient();
2079
- return this.runRequest(
2080
- () => client.request("hooks/list", {
2081
- cwds: (Array.isArray(cwds) ? cwds : [cwds])
2082
- .map(normalizeAgentText)
2083
- .filter(Boolean)
2084
- }),
2085
- "codex-app-server-hooks-list"
2086
- );
2087
- }
2088
-
2089
- async writeHookTrustState(state = {}) {
2090
- if (!isRecord(state) || Object.keys(state).length === 0) {
2091
- return null;
2092
- }
2093
- const client = await this.activeClient();
2094
- return this.runRequest(
2095
- () => client.request("config/batchWrite", {
2096
- edits: [{
2097
- keyPath: "hooks.state",
2098
- mergeStrategy: "upsert",
2099
- value: state
2100
- }],
2101
- expectedVersion: null,
2102
- filePath: null,
2103
- reloadUserConfig: true
2104
- }),
2105
- "codex-app-server-hook-trust-write"
2106
- );
2107
- }
2108
-
2109
- async unsubscribeThread(threadId = "") {
2110
- const client = await this.activeClient();
2111
- return this.runRequest(
2112
- () => client.request("thread/unsubscribe", {
2113
- threadId: normalizeAgentText(threadId)
2114
- }),
2115
- "codex-app-server-thread-unsubscribe"
2116
- );
2117
- }
2118
-
2119
- async deleteThread(threadId = "") {
2120
- const client = await this.activeClient();
2121
- return this.runRequest(
2122
- () => client.request("thread/delete", {
2123
- threadId: normalizeAgentText(threadId)
2124
- }),
2125
- "codex-app-server-thread-delete"
2126
- );
2127
- }
2128
-
2129
- async sendTurn(threadId = "", input = [], params = {}, { signal } = {}) {
2130
- const operation = async () => {
2131
- const client = await this.activeClient();
2132
- return client.request("turn/start", {
2133
- ...params,
2134
- input: codexTurnInput(input),
2135
- threadId: normalizeAgentText(threadId || params.threadId)
2136
- }, { signal });
2137
- };
2138
- const response = this.options.prepareThreadEnvironment
2139
- ? await this.withThreadEnvironment(threadId || params.threadId, {}, operation)
2140
- : await this.runRequest(operation, "codex-app-server-turn-start");
2141
- return {
2142
- ...normalizeAgentTurn({
2143
- id: response?.turn?.id || response?.turnId,
2144
- provider: CODEX_APP_SERVER_PROVIDER_ID,
2145
- raw: response?.turn || response
2146
- }),
2147
- response
2148
- };
2149
- }
2150
-
2151
- async steerTurn(threadId = "", turnId = "", input = [], params = {}, { signal } = {}) {
2152
- const client = await this.activeClient();
2153
- const response = await this.runRequest(
2154
- () => client.request("turn/steer", {
2155
- ...params,
2156
- expectedTurnId: normalizeAgentText(turnId || params.expectedTurnId),
2157
- input: codexTurnInput(input),
2158
- threadId: normalizeAgentText(threadId || params.threadId)
2159
- }, { signal }),
2160
- "codex-app-server-turn-steer"
2161
- );
2162
- return {
2163
- ...normalizeAgentTurn({
2164
- id: response?.turn?.id || response?.turnId || turnId,
2165
- provider: CODEX_APP_SERVER_PROVIDER_ID,
2166
- raw: response?.turn || response
2167
- }),
2168
- response
2169
- };
2170
- }
2171
-
2172
- interruptionOutcome(threadId = "", turnId = "") {
2173
- const key = JSON.stringify([normalizeAgentText(threadId), normalizeAgentText(turnId)]);
2174
- return this.interruptedTurns.get(key) || "";
2175
- }
2176
-
2177
- async interruptTurn(threadId = "", turnId = "", { outcome = "" } = {}) {
2178
- const client = await this.activeClient();
2179
- return this.runRequest(
2180
- () => this.#interruptTurn(client, normalizeAgentText(threadId), normalizeAgentText(turnId), undefined, outcome),
2181
- "codex-app-server-turn-interrupt"
2182
- );
2183
- }
2184
-
2185
- async #interruptTurn(client, threadId, turnId, signal, outcome = "") {
2186
- const key = JSON.stringify([threadId, turnId]);
2187
- this.interruptedTurns.set(key, outcome || this.interruptedTurns.get(key) || "");
2188
- const result = await client.request("turn/interrupt", { threadId, turnId }, { signal });
2189
- await Promise.all([...this.commandExecutions.values()]
2190
- .filter((command) => command.threadId === threadId && command.turnId === turnId)
2191
- .map((command) => this.#stopCommand(command)));
2192
- return result;
2193
- }
2194
-
2195
- #stopCommand(command) {
2196
- if (command.stopTask) return command.stopTask;
2197
- command.stopTask = (async () => {
2198
- const signal = AbortSignal.timeout(5000);
2199
- const request = (method, params) => command.client.request(method, params, { signal });
2200
- while (!command.completed) {
2201
- signal.throwIfAborted();
2202
- let cursor;
2203
- let terminal;
2204
- const cursors = new Set();
2205
- do {
2206
- const page = await request("thread/backgroundTerminals/list", {
2207
- threadId: command.threadId, limit: 100, ...(cursor ? { cursor } : {})
2208
- });
2209
- if (!Array.isArray(page.data)) throw new Error("Codex returned no command inventory.");
2210
- terminal = page.data.find((entry) => entry.processId === command.processId);
2211
- cursor = page.nextCursor;
2212
- if (cursor && cursors.has(cursor)) throw new Error("Codex repeated its command inventory cursor.");
2213
- cursors.add(cursor);
2214
- } while (!terminal && cursor);
2215
- if (command.completed) return;
2216
- if (terminal) {
2217
- // A recycled process ID must never target a different command.
2218
- if (terminal.itemId !== command.itemId) throw new Error("The native command identity changed during Stop.");
2219
- const stopped = await request("thread/backgroundTerminals/terminate", {
2220
- threadId: command.threadId, processId: command.processId
2221
- });
2222
- if (stopped.terminated === true) return;
2223
- }
2224
- // Codex emits command-start before adding it to the process inventory.
2225
- // Absence during that interval is not proof that the command exited.
2226
- await new Promise((resolve) => setTimeout(resolve, 25));
2227
- }
2228
- })().catch((cause) => {
2229
- const error = Object.assign(new Error("Codex could not confirm that the stopped turn's command exited.", { cause }), {
2230
- code: `${this.errorPrefix}codex_command_stop_unconfirmed`
2231
- });
2232
- this.commandStopFailure = error;
2233
- // Reuse the existing verified-runtime-stop owner when native control fails.
2234
- // That owner may already be awaiting this command while stopping a lost
2235
- // observation. Do not make command cleanup wait on its own caller.
2236
- void Promise.resolve(this.failObservation(error)).catch(() => null);
2237
- throw error;
2238
- });
2239
- return command.stopTask;
2240
- }
2241
-
2242
- nativeCliResumeCommand(threadId = "") {
2243
- const runtime = this.runtime || {};
2244
- return codexCliResumeCommand({
2245
- codexCommand: this.options.codexCommand || "codex",
2246
- endpoint: runtime.endpoint,
2247
- threadId
2248
- });
2249
- }
2250
-
2251
610
  close() {
2252
- this.options.onClose?.();
611
+ const { onClose = closeNothing } = this.options;
612
+ onClose();
2253
613
  for (const reject of this.settingsWaiters) reject(new Error("The assistant connection closed before confirming its settings."));
2254
614
  this.settingsWaiters.clear();
2255
615
  this.conversationRuntime.close();
2256
- this.threadControlProbes.clear();
2257
- this.commandExecutions.clear();
2258
- this.interruptedTurns.clear();
2259
- this.planUsage = null;
2260
- this.planUsagePending = null;
616
+ this.turns.clear();
617
+ this.auth.resetPlanUsage();
2261
618
  this.notificationSubscribers.clear();
2262
619
  this.threadRequestHandlers.clear();
2263
620
  this.client?.close();
2264
621
  this.client = null;
2265
622
  this.initializeResult = null;
2266
- this.helperAuth = null;
2267
- this.helperAuthBlocked = false;
623
+ this.auth.forgetHelper();
2268
624
  }
2269
625
 
2270
626
  async stopRuntime({
2271
- expectedAccountIdentitySignature = "",
2272
- preserveProcessExitProof = false
2273
- } = {}) {
627
+ expectedAccountIdentitySignature = "",
628
+ preserveProcessExitProof = false
629
+ } = {}) {
2274
630
  this.close();
2275
631
  this.runtimeStopOwner = this.runtime || this.runtimeStopOwner;
2276
632
  const runtime = this.runtimeStopOwner || {};
@@ -2294,19 +650,3 @@ export class CodexAppServerAgentProvider {
2294
650
  return result;
2295
651
  }
2296
652
  }
2297
-
2298
- export {
2299
- CODEX_APP_SERVER_INVALID_REQUEST_CODE,
2300
- ensureCodexAppServerThread, codexApplicationToolConfiguration, assertCodexApplicationToolSchemaIdentity, sendCodexAppServerPrompt,
2301
- codexAppServerDeliveredTurn,
2302
- codexAppServerEndpointForTarget, codexAppServerRequestIsInvalid, codexAppServerThreadIsMissing,
2303
- codexAppServerThreadHasReadableHistory, deleteCodexAppServerThread, deleteCodexAppServerHelperThread,
2304
- inspectCodexAppServerMessageAdmission,
2305
- codexCliResumeCommand, codexLocalImageInput, codexTextInput, codexTurnInput, shellQuote,
2306
- exportCodexNativeHistory, retireCodexConversationHistory, readCodexNativeGoal
2307
- };
2308
- export {
2309
- createCodexAppServerProviderOwner,
2310
- codexAppServerOwnedRuntimeKey,
2311
- codexAppServerRuntimeStopWasVerified
2312
- } from "./codexProviderOwner.js";