@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,27 +1,36 @@
1
1
  import { jsonHash as hash } from "../normalize.js";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import path from "node:path";
4
- import { open, realpath } from "node:fs/promises";
5
- import { CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES, codexCommandHookDefinition, createCodexAppServerRuntime } from "../codexProcess.js";
6
- import { CodexAppServerAgentProvider, codexLocalImageInput, ensureCodexAppServerThread, codexApplicationToolConfiguration,
7
- assertCodexApplicationToolSchemaIdentity, inspectCodexAppServerMessageAdmission, retireCodexConversationHistory } from "../codexProvider.js";
4
+ import { open as openFile, realpath } from "node:fs/promises";
5
+ import { createCodexAppServerRuntime } from "../codexRuntime.js";
6
+ import { inspectCodexAppServerMessageAdmission, retireCodexConversationHistory } from "../codexProviderThreads.js";
8
7
  import { createCodexAppServerProviderOwner } from "../codexProviderOwner.js";
9
- import { codexAuthOutputRequiresReconnect, codexProviderConfiguration, codexToolFreeConfiguration } from "../codexConfiguration.js";
10
- import { nativeAiProvider, nativeAiModel } from "../../../shared/nativeProviders.js";
11
- import { createCodexAppServerRunOwner, codexAppServerTurnState, codexAppServerFrozenTurnInterruptResponse } from "../codexTurn.js";
8
+ import { codexAuthOutputRequiresReconnect } from "../codexAccountReader.js";
9
+ import { createCodexAppServerRunOwner, codexAppServerTurnState } from "../codexTurn.js";
12
10
  import { createLocalConversationExecution } from "../localExecution.js";
13
- import { validateConversationConfiguration, validateConnectionModel } from "../configuration.js";
11
+ import { validateConversationConfiguration } from "../configuration.js";
14
12
  import { createScopedConversationHandle } from "./scoped.js";
15
13
  import { codexCommandHookCommand } from "../commandWrapper.js";
16
14
  import { isCompletedEnvelopeTurn } from "../transcript.js";
17
15
  import { createInertCodexConversationBinding } from "../runtimeStateUpgrade.js";
16
+ import { createCodexDriverConnection } from "./codexDriverConnection.js";
17
+ import { createCodexDriverTurns, goalValue } from "./codexDriverTurns.js";
18
+
19
+ /**
20
+ * The mutable state of one open driver conversation, shared by its connection and its turns.
21
+ * @typedef {object} CodexDriverSession
22
+ * @property {object} binding
23
+ * @property {Promise<void>} bindingWork
24
+ * @property {object | undefined} native
25
+ * @property {string | undefined} nativeProviderId
26
+ * @property {object | undefined} current
27
+ * @property {Promise<void> | undefined} stopping
28
+ * @property {boolean} disposed
29
+ * @property {object | undefined} preparedThreadSettings
30
+ * @property {Error | undefined} observationError
31
+ * @property {object | undefined} observationTarget
32
+ */
18
33
 
19
- const goalValue = goal => goal ? {
20
- id: hash([goal.threadId, goal.createdAt, goal.objective]), objective: goal.objective, status: goal.status,
21
- tokenBudget: goal.tokenBudget ?? null, tokensUsed: goal.tokensUsed, timeUsedSeconds: goal.timeUsedSeconds,
22
- ...(Number.isFinite(goal.createdAt) ? { createdAt: new Date(goal.createdAt * 1000).toISOString() } : {}),
23
- ...(Number.isFinite(goal.updatedAt) ? { updatedAt: new Date(goal.updatedAt * 1000).toISOString() } : {})
24
- } : null;
25
34
  const goalCapabilities = Object.freeze({ goals: true, goalBudgets: true,
26
35
  goalCommands: Object.freeze({
27
36
  set: Object.freeze({ delivery: "control", interruptsTurn: false }),
@@ -29,16 +38,31 @@ const goalCapabilities = Object.freeze({ goals: true, goalBudgets: true,
29
38
  pause: Object.freeze({ delivery: "control", interruptsTurn: true }),
30
39
  cancel: Object.freeze({ delivery: "control", interruptsTurn: false })
31
40
  }) });
32
- const nativeInput = input => [
33
- ...(input.text || input.attachmentManifest ? [{ type: "text", text: `${input.text || ""}${input.attachmentManifest || ""}` }] : []),
34
- ...(input.content || []).map(part => part.type === "text" ? part :
35
- { type: "image", url: `data:${part.mediaType};base64,${part.image.toString("base64")}` }),
36
- ...codexLocalImageInput(input.localFiles)
37
- ];
38
41
 
39
- /** Conversations own threads and observers on the account's shared native runtime. */
40
- export function createCodexConversationDriver({ connections, host = {}, limits = {} } = {}) {
41
- const env = { ...(host.env || process.env) };
42
+ /**
43
+ * The conversation connections an application authorizes models through.
44
+ * @typedef {object} CodexConversationConnections
45
+ * @property {(request: { context: object, integrationId: string }) => Promise<object>} resolve
46
+ */
47
+
48
+ // input validation: the driver's own conversations run Codex in the application's environment.
49
+ function assertCodexApplicationEnvironment(host) {
50
+ if (!host.env) throw new TypeError("createCodexConversationDriver requires the application's environment, such as process.env.");
51
+ }
52
+
53
+ /**
54
+ * Conversations own threads and observers on the account's shared native runtime.
55
+ * @param {object} dependencies
56
+ * @param {CodexConversationConnections} [dependencies.connections]
57
+ * @param {Record<string, any>} [dependencies.host] The application's host description. Its env, the application's
58
+ * environment such as process.env, is required by the driver's own conversations.
59
+ * @param {object} [dependencies.limits]
60
+ */
61
+ // readable-check: allow dependency-default - data: a runtime without host settings runs Codex in its working directory with local execution
62
+ export function createCodexConversationDriver({ connections, host = {}, limits = {} }) {
63
+ // A host that opens its own conversations may pass no environment; the
64
+ // driver's own conversations require it.
65
+ const env = { ...host.env };
42
66
  const workdir = path.resolve(host.workdir || process.cwd());
43
67
  const configRoot = path.resolve(workdir, env.CODEX_HOME || path.join(env.HOME || homedir(), ".codex"));
44
68
  env.CODEX_HOME = configRoot;
@@ -57,7 +81,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
57
81
  let auth = {};
58
82
  let file;
59
83
  try {
60
- file = await open(path.join(configRoot, "auth.json"), "r");
84
+ file = await openFile(path.join(configRoot, "auth.json"), "r");
61
85
  const bytes = Buffer.alloc(1024 * 1024 + 1);
62
86
  let length = 0;
63
87
  while (length < bytes.length) {
@@ -90,14 +114,16 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
90
114
  }
91
115
 
92
116
  async function nativeHost(selected) {
93
- // Managed applications supply their existing execution/account scope. This
117
+ // Managed applications supply their own execution and account scope. This
94
118
  // facility contains host policy; the provider and process algorithms stay here.
95
119
  if (host.codex) return host.codex({ providerId: selected.providerId });
120
+ // readable-check: allow method-probe - input validation: the application's execution host must run native processes
96
121
  if (typeof execution.run !== "function") {
97
122
  throw new TypeError("The Codex execution host must support capture and detached native execution.");
98
123
  }
99
124
  const processExecution = { ...execution, run: request => execution.run({ ...request, limits: host.limits }) };
100
125
  const scope = hash([configRoot, selected.providerId, selected.accountIdentity || "", command, host.commandWrapper || null]).slice(0, 16);
126
+ // readable-check: allow optional-call - platform object: process.getuid exists only on POSIX platforms
101
127
  const runtimeDir = path.join(host.runtimeDirectory || path.join(tmpdir(), `jskit-${process.getuid?.() ?? "user"}`),
102
128
  `codex-app-server-${scope}`);
103
129
  async function generation() {
@@ -142,1096 +168,492 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
142
168
  return native.runOwner.closeSession(sessionId, native.preparation.cleanup(options));
143
169
  }
144
170
 
145
- return Object.freeze({
146
- // Codex fixes dynamic tool definitions when the thread is created. The three
147
- // discovery entry points stay stable while the authorized action set changes.
148
- toolDiscovery: true,
149
- canonicalTranscript: true,
150
- attachmentTypes: ["text", "image", "localFile"],
151
- capabilities: Object.freeze({ streaming: true, cancellation: true, history: true,
152
- instructions: true, configuration: true, steering: true, ...goalCapabilities,
153
- attachments: true, tools: true, nativeTools, structuredOutput: true }),
154
- validateConfiguration(config) {
155
- validateConversationConfiguration(config, { engine: "Codex", connections, structuredOutput: true, maxOutputCharacters: maximumOutput });
156
- },
157
- disposeNative,
158
- interruptDetachedConversation({ sessionId, native, input, context }) {
159
- return native.runOwner.interruptDetachedTurn(sessionId, input, context);
160
- },
161
- deleteDetachedConversation({ sessionId, native, input, context }) {
162
- return native.runOwner.deleteDetachedThread(sessionId, input, context);
163
- },
164
- listConversationStorage(prepared) {
165
- const { binding } = prepared;
166
- return withNativeStorageProvider(prepared, async provider =>
167
- (await provider.listNativeThreadsForCwd(binding.workdir)).map((conversationId) => ({ ...binding, conversationId })));
168
- },
169
- retireConversationHistory(prepared) {
170
- return withNativeStorageProvider(prepared, (provider, toolHomeSource) =>
171
- retireCodexConversationHistory(provider, prepared.binding, {
172
- toolHomeSource, beforeDelete: prepared.context.beforeDelete, signal: prepared.context.signal,
173
- errorPrefix: prepared.native.errorPrefix
174
- }));
175
- },
176
- invalidateRuntimes({ native, input }) {
177
- return native.runOwner.invalidateRuntimes(input, native.preparation);
178
- },
179
- reconcileSessions({ native, sessions, options }) {
180
- return native.runOwner.reconcileSessions(sessions, options, native.preparation);
181
- },
182
- unsubscribeSessions({ native, sessions }) {
183
- return native.runOwner.unsubscribeSessions(sessions, native.preparation);
184
- },
185
- async closeProject({ native, input }) {
186
- const prepared = await native.preparation.projectCleanup();
187
- if (Object.hasOwn(prepared, "value")) return prepared.value;
188
- return native.runOwner.closeProject(input, prepared);
189
- },
190
- inspectTemporaryActivity({ sessionId, native }) {
191
- return native.runOwner.hasActiveTemporaryConversation(sessionId);
192
- },
193
- releaseRenewalPredecessorProcessExitProof({ native, context }) {
194
- const prepared = native.preparation.predecessor(context);
195
- return native.providerOwner.releasePreparedRuntimeProof(prepared);
196
- },
197
- releaseRenewalSuccessorProcessExitProof({ native, context }) {
198
- const prepared = native.preparation.successor(context);
199
- return native.providerOwner.releasePreparedRuntimeProof(prepared);
200
- },
201
- async generateRenewalHandover({ sessionId, native, input, context }) {
202
- const prepared = native.preparation.handover(input, context);
203
- const acquired = await native.runOwner.conversationContext(sessionId, input, prepared.context);
204
- if (acquired.ok === false) return acquired;
205
- const turn = await prepared.turn(acquired);
206
- const execution = await native.runOwner.runRenewalHandover(sessionId, turn.input, turn.context);
207
- return turn.completeResult(execution);
208
- },
209
- async seedRenewalHandover({ sessionId, native, input, context }) {
210
- const prepared = native.preparation.seed(input, context);
211
- const acquired = await native.runOwner.conversationContext(sessionId, input, prepared.context);
212
- if (acquired.ok === false) return acquired;
213
- const turn = prepared.turn(acquired);
214
- const execution = await native.runOwner.runRenewalSeed(turn.input, turn.context);
215
- return turn.completeResult(execution);
216
- },
217
- async ensureConversation({ sessionId, native }) {
218
- return native.runOwner.ensureSessionReadiness(sessionId, await native.preparation.readiness());
219
- },
220
- async updateConversationSettings({ sessionId, native, context, input }) {
221
- // Changing a thread's settings is not a use that keeps Codex running.
222
- const borrow = { release() {} };
223
- try {
224
- const acquired = await native.runOwner.conversationContext(sessionId, { conversationId: input.threadId }, context, borrow);
225
- if (acquired.ok === false) return acquired;
226
- await acquired.provider.updateThreadSettings(input.threadId, { model: input.model, effort: input.effort });
227
- return { ok: true };
228
- } finally {
229
- borrow.release();
230
- }
231
- },
232
- async createConversation({ sessionId, native, input, context }) {
233
- return native.runOwner.createConversation(sessionId, input, context);
234
- },
235
- async runDetachedConversation({ sessionId, native, input, options }) {
236
- return native.runOwner.runDetachedConversation(sessionId, input, options);
237
- },
238
- async createBinding() {
239
- return createInertCodexConversationBinding({ workdir: await realpath(workdir), configRoot });
240
- },
241
- async open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
242
- const supplied = conversation?.native;
243
- if (supplied?.scoped) {
244
- const { conversationId, context: openingContext } = supplied.scoped;
245
- const { runOwner: owner } = supplied;
246
- const sessionId = conversation.sessionId;
247
- // These are the original scoped commands, including their native
248
- // start dedup, watcher/deadline and Stop/delete proof. No main journal
249
- // or additional command queue is introduced for this representation.
250
- return createScopedConversationHandle({ conversationId,
251
- sendNative: (input, context) => owner.startConversationTurn(sessionId, input, context),
252
- readNative: (input, context) => owner.readConversation(sessionId, input, context),
253
- waitNative: (input, context) => owner.waitForConversationTurn(sessionId, input, context),
254
- cancel: (input, context) => owner.stopConversation(sessionId, input, context),
255
- deleteConversation: (input, context) => owner.deleteConversation(sessionId, input, context),
256
- // Runtime shutdown retains the parent's cleanup receipt. Explicit
257
- // scoped deletion above owns native deletion and its proof.
258
- disposeOpening: () => disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options: openingContext })
259
- });
260
- }
261
- if (!supplied && (!binding || typeof binding.threadId !== "string" || binding.workdir !== await realpath(workdir) || binding.configRoot !== configRoot)) {
262
- throw new Error("This Codex conversation belongs to another working directory or credential home. Restore its original host configuration.");
263
- }
264
- let native;
265
- let nativeProviderId;
266
- let current;
267
- let stopping;
268
- let disposed = false;
269
- let preparedThreadSettings;
270
- let bindingWork = Promise.resolve();
271
- const providerKey = "conversation";
272
- const providerOwner = supplied?.providerOwner || createCodexAppServerProviderOwner({
273
- onRecoveryEvent(_owner, event, error) {
274
- if (event === "stopFailed") current?.completion.reject(error);
275
- }
276
- });
277
- if (!conversation?.runtime?.store) throw new TypeError("Codex requires its bound conversation store.");
278
- const sessionId = conversation.sessionId;
279
- const nativeStore = conversation.runtime.store;
280
- const boundedWriter = name => (id, input) => {
281
- const limit = name === "writeConversationThinkingMessage" ? 4 * 1024 * 1024
282
- : name.includes("AssistantMessage") ? Math.min(maximumOutput, maximumFinalReply) : maximumOutput;
283
- const length = input.text?.length || 0;
284
- if (length <= limit) return nativeStore[name](id, input);
285
- if (!name.includes("AssistantMessage") || length > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
286
- return nativeStore.mutateSession(id, async transaction => {
287
- const { runtime: state } = await transaction.readMetadata();
288
- const identity = input.nativeIdentity;
289
- const run = state.binding?.codexAppServerRun;
290
- if (identity?.threadId && identity.turnId && state.engine === "codex" &&
291
- state.binding.threadId === identity.threadId && run?.providerThreadId === identity.threadId &&
292
- run.providerTurnId === identity.turnId && run.outerTurnId) {
293
- for (const turnId of (await transaction.listTurnIds()).reverse()) {
294
- const turn = await transaction.readTurn(turnId);
295
- if (turn?.system?.messageId !== run.outerTurnId) continue;
296
- if (turn.system.origin === "application" && !turn.system.goal && isCompletedEnvelopeTurn(turn) &&
297
- turn.metadata.runtime.engine === "codex" && turn.metadata.runtime.segmentId === state.segmentId &&
298
- turn.metadata.runtime.nativeTurnId === identity.turnId) {
299
- return nativeStore[name](id, input);
300
- }
301
- break;
302
- }
303
- }
304
- throw new Error("Codex output exceeded the configured limit.");
305
- });
306
- };
307
- const runtime = supplied ? conversation.runtime : { ...conversation.runtime, store: { ...nativeStore,
308
- writeConversationAssistantMessage: boundedWriter("writeConversationAssistantMessage"),
309
- upsertConversationAssistantMessage: boundedWriter("upsertConversationAssistantMessage"),
310
- writeConversationCommentaryMessage: boundedWriter("writeConversationCommentaryMessage"),
311
- writeConversationThinkingMessage: boundedWriter("writeConversationThinkingMessage"),
312
- async updateConversationStream(id, input) {
313
- if ((input.delta?.length || input.text?.length || 0) > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
314
- const stream = await nativeStore.updateConversationStream(id, input);
315
- if (stream?.messages.some(message => message.text.length > maximumOutput)) throw new Error("Codex output exceeded the configured limit.");
316
- return stream;
317
- },
318
- writeConversationUserMessage(id, input) {
319
- const pending = owner.pendingUserMessages.get(`${sessionId}\0${input.messageId}`);
320
- return conversation.runtime.store.writeConversationUserMessage(id, {
321
- ...input, authoredRequest: pending?.authoredInput
322
- });
323
- }
324
- } };
325
- const owner = supplied?.runOwner || createCodexAppServerRunOwner({
326
- persistCommentary: conversation.persistCommentary,
327
- finalizingGraceMs: limits.codexFinalizingGraceMs,
328
- finalizingGraceAfterHistoryRead: limits.codexFinalizingGraceAfterHistoryRead,
329
- failureDetailGraceMs: limits.codexFailureDetailGraceMs,
330
- createRuntime: async () => runtime,
331
- createStore: async () => runtime.store,
332
- acquireProvider: async () => native,
333
- hasRuntime: () => Boolean(binding.threadId),
334
- publish: (_id, event) => publishNative(event),
335
- checkpoint: (_id, input) => conversation.checkpoint({ ...input,
336
- status: input.turnOutcome === "response_delivery_failure" ? "failed" : input.status }),
337
- async onNotificationSignal(kind, { threadId, turnId, provider, error }) {
338
- const active = current;
339
- if (kind !== "provider_error" || !active?.dispatched || active.finished || provider !== native ||
340
- threadId !== binding.threadId || !turnId || active.turnId && active.turnId !== turnId) return;
341
- const tracked = codexAppServerTurnState(await runtime.getSession(sessionId));
342
- if (tracked.threadId !== threadId || tracked.turnId !== turnId) return;
343
- // The original detached waiter recovered only this admitted turn.
344
- // Keep native activity truthful; run's existing cleanup owns Stop.
345
- await owner.recoverActiveTurn(sessionId, { provider, retryOnError: false, providerError: { threadId, turnId, error } });
346
- if (current !== active || active.finished || active.turnId !== turnId ||
347
- provider !== native || threadId !== binding.threadId) return;
348
- const result = await owner.stopTurnWithProviderFailure(sessionId, threadId, turnId, { provider, error, failureDetailReady: true });
349
- if (current === active && active.turnId === turnId && provider === native &&
350
- threadId === binding.threadId && result.reason === "provider_still_active") {
351
- active.completion.reject(Object.assign(new Error(error), { status: "failed" }));
352
- }
353
- }
354
- });
355
- // Retain this queue's exact opening namespace, never its admission grant.
356
- const drainNotifications = owner.notificationQueue.captureDrain(sessionId);
357
- async function publishNative(event) {
358
- if (supplied) binding.threadId = await conversation.identity.read();
359
- await conversation.publish(event);
360
- if (event.nativeGoal?.threadId === binding.threadId) {
361
- await recordGoal(event.nativeGoal.goal);
362
- }
363
- const turn = event.payload?.agentSession?.turn;
364
- if (!turn || !current?.dispatched) return;
365
- if (turn.active && current.finished && current.finishIfCurrent) {
366
- // The original command owner may start the next turn during Send.
367
- // Its publication replaces only this adapter's settled waiter.
368
- current.completion = Promise.withResolvers();
369
- current.completion.promise.catch(() => {});
370
- current.finished = false;
371
- }
372
- if (turn.id && turn.id !== current.turnId) {
373
- const previous = current.turnId;
374
- current.turnId = turn.id;
375
- if (current.admitted) {
376
- if (previous) await current.onNativeTurn({ turnId: turn.id, nativeOwner: true });
377
- } else current.pendingNativeTurn = turn.id;
378
- }
379
- if (!turn.active && event.reason === "codex-app-server-turn-idle") {
380
- current.finished = true;
381
- if ((["failed", "interrupted"].includes(turn.status) || !supplied && turn.error) && !current.signal.aborted) {
382
- current.completion.reject(Object.assign(new Error(turn.error || `Codex turn ${turn.status}.`), {
383
- status: turn.status === "completed" ? "failed" : turn.status }));
384
- } else current.completion.resolve();
385
- }
386
- }
387
- const savedTurn = codexAppServerTurnState({ agentRuns: binding.codexAppServerRun ? [binding.codexAppServerRun] : [] });
388
- let observationError = binding.observationLoss && (!binding.observationLoss.stopped ||
389
- savedTurn.active && savedTurn.status === "observation_lost")
390
- ? Object.assign(new Error(binding.observationLoss.message), { cleanupFailed: true }) : null;
391
- if (observationError) await reportFailure(observationError);
392
- function updateBinding(patch) {
393
- const work = bindingWork.then(async () => {
394
- const next = { ...binding, ...patch };
395
- await writeBinding(next);
396
- binding = next;
397
- });
398
- bindingWork = work.catch(() => {});
399
- return work;
400
- }
401
- async function recordGoal(goal) {
402
- if (supplied) binding.goal = goal;
403
- else await updateBinding({ goal });
404
- if (current?.admitted) await current.onEvent({ type: "goal", goal: goalValue(goal) });
405
- else if (current) current.pendingGoal = goal;
406
- else await conversation.publish({ payload: { goal: goalValue(goal) }, reason: "codex-goal" });
407
- return goal;
408
- }
171
+ function validateConfiguration(config) {
172
+ validateConversationConfiguration(config, { engine: "Codex", connections, structuredOutput: true, maxOutputCharacters: maximumOutput });
173
+ }
409
174
 
410
- let observationTarget;
411
- const observation = supplied ? null : owner.createObservation(sessionId, {
412
- target(error) {
413
- if (observationTarget?.error !== error) observationTarget = { error, threadId: binding.threadId,
414
- message: "Codex observation was lost. Work is stopped; use Resume or Send to continue.",
415
- pendingMessage: "Codex observation was lost. A stop is not yet confirmed." };
416
- return observationTarget;
417
- },
418
- retireProvider: () => providerOwner.closeProvider(providerKey)
419
- });
175
+ function interruptDetachedConversation({ sessionId, native, input, context }) {
176
+ return native.runOwner.interruptDetachedTurn(sessionId, input, context);
177
+ }
420
178
 
421
- async function recordObservationBarrier({ provider, error }) {
422
- observationError = error;
423
- // Keep admission unavailable in memory even if its durable write fails.
424
- await reportFailure(error);
425
- await updateBinding({ observationLoss: {
426
- message: error.message, providerId: nativeProviderId,
427
- runtime: provider.runtime || provider.runtimeStopOwner || binding.observationLoss?.runtime,
428
- stopped: false
429
- } });
430
- }
179
+ function deleteDetachedConversation({ sessionId, native, input, context }) {
180
+ return native.runOwner.deleteDetachedThread(sessionId, input, context);
181
+ }
431
182
 
432
- async function completeObservation({ provider, error, sharedStop, threadIds }) {
433
- if (threadIds && !threadIds.includes(binding.threadId)) return;
434
- await updateBinding({ observationLoss: { ...binding.observationLoss, stopped: true },
435
- ...(sharedStop ? { executionId: "" } : {}) });
436
- await observation.complete({ provider, error, sharedStop, threadIds });
437
- observationError = null;
438
- await reportFailure(error, { recovered: true });
439
- current?.completion.reject(error);
440
- }
183
+ function listConversationStorage(prepared) {
184
+ const { binding } = prepared;
185
+ return withNativeStorageProvider(prepared, async provider =>
186
+ (await provider.listNativeThreadsForCwd(binding.workdir)).map((conversationId) => ({ ...binding, conversationId })));
187
+ }
441
188
 
442
- function acquireProvider(facilities, providerId) {
443
- nativeProviderId = providerId;
444
- const providerOptions = {
445
- ...facilities.options, runtimeDir: facilities.parameters.runtimeDir, modelProviderId: providerId,
446
- threadEnv: env,
447
- readInstructions: () => preparedThreadSettings?.developerInstructions,
448
- prepareThreadEnvironment: environment => preparedThreadSettings?.config.shell_environment_policy.set || environment,
449
- prepareThreadResumeParams(threadId, params) {
450
- if (threadId !== binding.threadId || !preparedThreadSettings) {
451
- throw new Error("Codex requires current authorized settings before resuming this conversation.");
452
- }
453
- return { ...preparedThreadSettings, ...params,
454
- config: { ...preparedThreadSettings.config, ...params.config } };
455
- },
456
- async beforeResumeThread(threadId) {
457
- owner.assertThreadCanResume(await owner.readAgentRunForSession(runtime.store, sessionId), threadId);
458
- },
459
- requestTimeoutMs: limits.admissionTimeoutMs || 30_000
460
- };
461
- return providerOwner.withLifecycle(() => providerOwner.createProvider({
462
- providerKey, providerOptions, preserveProcessExitProof: true,
463
- resources: {
464
- release(provider) {
465
- if (native === provider) native = null;
466
- const subscriptionPrefix = `${providerKey}:`;
467
- for (const key of [...owner.eventSubscriptions.keys()]) {
468
- if (key.startsWith(subscriptionPrefix)) owner.unsubscribeEventSubscription(key);
469
- }
470
- },
471
- invalidation: {
472
- async prepare(input) {
473
- if (native !== input.provider) return false;
474
- await recordObservationBarrier(input);
475
- await observation.barrier(input);
476
- },
477
- async complete({ provider, error, preparationError, stopped }) {
478
- try {
479
- if (preparationError) throw preparationError;
480
- if (stopped.status !== "fulfilled") throw stopped.reason;
481
- await completeObservation({ provider, error, sharedStop: true });
482
- } catch (failure) {
483
- error.cleanupFailed = true;
484
- error.cause = failure;
485
- observationError = error;
486
- await reportFailure(error);
487
- current?.completion.reject(error);
488
- throw failure;
489
- }
490
- }
491
- }
492
- },
493
- create: onObservationLost => new CodexAppServerAgentProvider({ ...providerOptions, onObservationLost }, facilities),
494
- observation: {
495
- async prepare(input) {
496
- if (input.threadIds && !input.threadIds.includes(binding.threadId)) return new Map();
497
- observationError = input.error;
498
- await reportFailure(input.error);
499
- const threads = await observation.prepare(input);
500
- await recordObservationBarrier(input);
501
- return threads;
502
- },
503
- async barrier(input) {
504
- if (input.threadIds && !input.threadIds.includes(binding.threadId)) return;
505
- await observation.barrier(input);
506
- await recordObservationBarrier(input);
507
- },
508
- complete: completeObservation
509
- }
510
- }), providerKey, providerOptions);
511
- }
189
+ function retireConversationHistory(prepared) {
190
+ return withNativeStorageProvider(prepared, (provider, toolHomeSource) =>
191
+ retireCodexConversationHistory(provider, prepared.binding, {
192
+ toolHomeSource, beforeDelete: prepared.context.beforeDelete, signal: prepared.context.signal,
193
+ errorPrefix: prepared.native.errorPrefix
194
+ }));
195
+ }
512
196
 
513
- async function stop() {
514
- if (stopping) return stopping;
515
- stopping = (async () => {
516
- if (!native && observationError) {
517
- // Recover the exact saved owner through the production disk lock
518
- // and metadata path. Cleanup must not start or resume a thread.
519
- const saved = binding.observationLoss;
520
- const facilities = await nativeHost({ providerId: saved.providerId,
521
- accountIdentity: binding.connectionIdentities?.[saved.providerId] });
522
- native = await acquireProvider(facilities, saved.providerId);
523
- native.runtimeStopOwner = saved.runtime;
524
- native.observationFailure = observationError;
525
- }
526
- if (native?.observationFailure) {
527
- await providerOwner.recover(providerKey, native.observationFailure);
528
- return;
529
- }
530
- if (native && binding.threadId) {
531
- const provider = native;
532
- try {
533
- const { goal } = await provider.stopThreadForObservationLoss(binding.threadId, current?.turnId);
534
- await recordGoal(goal || null);
535
- } catch (error) {
536
- await provider.failObservation(error);
537
- }
538
- }
539
- })().catch(error => { error.cleanupFailed = true; throw error; }).finally(() => { stopping = null; });
540
- return stopping;
541
- }
197
+ function invalidateRuntimes({ native, input }) {
198
+ return native.runOwner.invalidateRuntimes(input, native.preparation);
199
+ }
542
200
 
543
- async function selection(configuration, context, signal, requireBound = false, receiptBinding) {
544
- if (!configuration.integrationId) {
545
- if (requireBound && !receiptBinding?.accountIdentity) throw new Error("The completed Codex response has no saved native account fingerprint.");
546
- if (requireBound && binding.accountIdentity !== receiptBinding.accountIdentity) {
547
- throw new Error("The completed Codex response belongs to a different native account binding.");
548
- }
549
- return { model: configuration.model, providerId: "openai" };
550
- }
551
- const connection = await connections.resolve({ context, integrationId: configuration.integrationId });
552
- signal.throwIfAborted();
553
- const provider = nativeAiProvider(connection.providerId);
554
- const model = nativeAiModel(connection.model, connection.providerId);
555
- if (!provider || !model) throw new Error("This exact provider/model is not supported by the Codex integration. Choose a supported model without renaming it.");
556
- validateConnectionModel(configuration, connection);
557
- if (configuration.effort && !model.variants.includes(configuration.effort)) throw new Error("The selected Codex model does not support this reasoning effort.");
558
- const identity = hash([provider.id, provider.baseUrl, connection.apiKey]);
559
- const previous = binding.connectionIdentities?.[provider.id];
560
- const expected = receiptBinding?.connectionIdentities?.[provider.id];
561
- if (requireBound && !expected) throw new Error("The completed Codex response has no saved provider account fingerprint.");
562
- if (requireBound && (previous !== expected || identity !== expected)) {
563
- throw new Error("The completed Codex response belongs to a different provider account binding.");
564
- }
565
- if (previous && previous !== identity) throw new Error("This conversation belongs to another Codex provider account. Restore that connection or start a new conversation.");
566
- if (!previous) await updateBinding({ connectionIdentities: { ...binding.connectionIdentities, [provider.id]: identity } });
567
- return { model: model.id, providerId: provider.id, accountIdentity: identity, config: codexProviderConfiguration(provider, connection.apiKey) };
568
- }
201
+ function reconcileSessions({ native, sessions, options }) {
202
+ return native.runOwner.reconcileSessions(sessions, options, native.preparation);
203
+ }
569
204
 
570
- async function connect(signal, selected) {
571
- if (observationError) throw observationError;
572
- if (native && nativeProviderId !== selected.providerId) {
573
- await stop();
574
- providerOwner.closeProvider(providerKey);
575
- }
576
- if (!native) {
577
- const facilities = await nativeHost(selected);
578
- signal.throwIfAborted();
579
- const provider = await acquireProvider(facilities, selected.providerId);
580
- // Account startup has other waiters. Cancellation releases this
581
- // observer; the existing runtime owner still settles shared startup.
582
- const connected = providerOwner.acquireRuntime({ providerKey, provider,
583
- providerOptions: provider.options, operation: () => provider.connect() });
584
- const cancelled = Promise.withResolvers();
585
- const abort = () => cancelled.reject(signal.reason);
586
- signal.addEventListener("abort", abort, { once: true });
587
- if (signal.aborted) abort();
588
- try {
589
- await Promise.race([connected, cancelled.promise]);
590
- signal.throwIfAborted();
591
- // Execution identity is a reference for recovery, not ownership of
592
- // the account service by this one conversation.
593
- await updateBinding({ executionId: provider.runtime.executionId });
594
- native = provider;
595
- nativeProviderId = selected.providerId;
596
- } catch (error) {
597
- void connected.then(() => providerOwner.closeProvider(providerKey), () => providerOwner.closeProvider(providerKey));
598
- throw error;
599
- } finally {
600
- signal.removeEventListener("abort", abort);
601
- }
602
- }
603
- signal.throwIfAborted();
604
- if (selected.providerId !== "openai") return;
605
- const identity = await accountIdentity(native.client, signal);
606
- if (binding.accountIdentity && binding.accountIdentity !== identity) {
607
- providerOwner.closeProvider(providerKey);
608
- throw new Error("This conversation belongs to another Codex account. Restore that account or start a new conversation.");
609
- }
610
- if (!binding.accountIdentity) await updateBinding({ accountIdentity: identity });
611
- }
205
+ function unsubscribeSessions({ native, sessions }) {
206
+ return native.runOwner.unsubscribeSessions(sessions, native.preparation);
207
+ }
612
208
 
613
- async function prepareConfiguration(configuration, context, signal, tools) {
614
- const { dynamicTools, toolSchemaIdentity } = codexApplicationToolConfiguration(tools);
615
- assertCodexApplicationToolSchemaIdentity(binding.threadId, binding.toolSchemaIdentity, toolSchemaIdentity);
616
- const selected = await selection(configuration, context, signal);
617
- await connect(signal, selected);
618
- const [configResult, hookResult] = await Promise.all([
619
- native.client.request("config/read", { cwd: binding.workdir, includeLayers: false }, { signal }),
620
- native.client.request("hooks/list", { cwds: [binding.workdir] }, { signal })
621
- ]);
622
- const toolConfiguration = codexToolFreeConfiguration({ configResult, hookResult, workdir: binding.workdir });
623
- // Native control verification runs the host's shell even when model
624
- // shell tools are disabled. Its login profile needs the standard tools;
625
- // do not copy credentials or the rest of the host environment here.
626
- toolConfiguration.shell_environment_policy.set.PATH = env.PATH || "/usr/bin:/bin";
627
- // A goal can be set while an ordinary turn is already running. Codex
628
- // must have its native scheduler enabled before that turn starts.
629
- toolConfiguration.features.goals = true;
630
- if (host.commandWrapper) {
631
- const hookCommand = codexCommandHookCommand(host.commandWrapper);
632
- const owned = hookResult.data[0].hooks.filter(hook => hook.source === "sessionFlags" &&
633
- hook.eventName === "preToolUse" && hook.handlerType === "command" && hook.command === hookCommand);
634
- if (owned.length !== 1 || typeof owned[0].currentHash !== "string" || !owned[0].currentHash) {
635
- throw new Error("Codex did not install the host's required command wrapper.");
636
- }
637
- toolConfiguration.features.hooks = true;
638
- toolConfiguration.hooks.PreToolUse = codexCommandHookDefinition(hookCommand);
639
- toolConfiguration.hooks.state[owned[0].key] = { enabled: true, trusted_hash: owned[0].currentHash };
640
- }
641
- if (nativeTools) {
642
- Object.assign(toolConfiguration.features, { shell_tool: true, unified_exec: true, view_image: true });
643
- toolConfiguration.include_environment_context = true;
644
- toolConfiguration.include_permissions_instructions = true;
645
- toolConfiguration.tools.update_plan.enabled = true;
646
- toolConfiguration.shell_environment_policy.set = Object.fromEntries(Object.entries(env).filter(([name, value]) =>
647
- typeof value === "string" && !CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES.has(name)));
648
- toolConfiguration.web_search = selected.config?.web_search || "disabled";
649
- }
650
- const params = { developerInstructions: configuration.systemPrompt,
651
- cwd: binding.workdir, modelProvider: selected.providerId, ...(selected.model ? { model: selected.model } : {}),
652
- dynamicTools, ...(!nativeTools ? { environments: [], runtimeWorkspaceRoots: [], selectedCapabilityRoots: [] } : {}),
653
- approvalPolicy: "never", sandbox: nativeTools ? "danger-full-access" : "read-only", allowProviderModelFallback: false,
654
- config: { ...selected.config, ...toolConfiguration }
655
- };
656
- // Cold control recovery uses the provider's original settings facilities;
657
- // it must not resume with ambient defaults or an old instruction prompt.
658
- preparedThreadSettings = params;
659
- return { selected, params, toolSchemaIdentity };
660
- }
209
+ async function closeProject({ native, input }) {
210
+ const prepared = await native.preparation.projectCleanup();
211
+ if (Object.hasOwn(prepared, "value")) return prepared.value;
212
+ return native.runOwner.closeProject(input, prepared);
213
+ }
661
214
 
662
- async function prepare(configuration, context, signal, tools) {
663
- const { selected, params, toolSchemaIdentity } = await prepareConfiguration(configuration, context, signal, tools);
664
- const retainedThreadId = binding.threadId;
665
- const prepared = await ensureCodexAppServerThread({
666
- provider: native, workdir: binding.workdir,
667
- observeThread: threadId => owner.subscribeEvents(sessionId, native, threadId, { providerKey }),
668
- settings: async () => ({ threadSettings: params, threadStartSettings: params }),
669
- identity: { read: () => retainedThreadId,
670
- async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
671
- });
672
- if (!retainedThreadId && prepared.thread.raw?.historyMode !== "paginated") {
673
- throw new Error("Codex did not create the required paginated history. Update Codex before starting this conversation.");
674
- }
675
- owner.subscribeEvents(sessionId, native, prepared.threadId, { providerKey });
676
- return { ...selected, resumeOptions: params };
677
- }
215
+ function inspectTemporaryActivity({ sessionId, native }) {
216
+ return native.runOwner.hasActiveTemporaryConversation(sessionId);
217
+ }
678
218
 
679
- function deliveryInput(input, beforeDispatch, rejected) {
680
- if (supplied) return { ...input.nativeMessage,
681
- onPromptSending: beforeDispatch,
682
- onPromptRejected: async () => { await input.nativeMessage.onPromptRejected?.(); await rejected?.(); } };
683
- return { messageId: input.messageId, message: input.text,
684
- displayMessage: input.authoredInput?.text ?? input.text,
685
- displayAttachments: input.attachments, attachments: input.localFiles,
686
- onPromptSending: value => beforeDispatch({ threadId: value.threadId }), onPromptRejected: rejected };
687
- }
219
+ function releaseRenewalPredecessorProcessExitProof({ native, context }) {
220
+ const prepared = native.preparation.predecessor(context);
221
+ return native.providerOwner.releasePreparedRuntimeProof(prepared);
222
+ }
688
223
 
689
- async function deliveredRow(input, nativeIdentity) {
690
- const written = await owner.writeDeliveredUserMessage(runtime, sessionId,
691
- input.authoredInput?.text ?? input.text, input.messageId, null, input.attachments,
692
- nativeIdentity);
693
- if (written) return written;
694
- const saved = (await runtime.store.readConversationLog(sessionId)).find(turn =>
695
- (turn.user || turn.system)?.messageId === input.messageId);
696
- if (!saved) throw new Error("The accepted native receipt has no authored conversation message.");
697
- return saved;
698
- }
224
+ function releaseRenewalSuccessorProcessExitProof({ native, context }) {
225
+ const prepared = native.preparation.successor(context);
226
+ return native.providerOwner.releasePreparedRuntimeProof(prepared);
227
+ }
699
228
 
700
- async function withDelivery(command, operation) {
701
- const input = command.input;
702
- return owner.withMessageDelivery(sessionId, input.messageId, {
703
- attachments: input.attachments, text: input.authoredInput?.text ?? input.text,
704
- turnMetadata: input.nativeMessage?.turnMetadata || null, actorContext: input.nativeMessage?.actorContext || null,
705
- authoredInput: input.authoredInput,
706
- onNativeReceipt: command.onNativeReceipt,
707
- onReceiptPersisted: command.onReceiptPersisted
708
- }, operation);
709
- }
229
+ async function generateRenewalHandover({ sessionId, native, input, context }) {
230
+ const prepared = native.preparation.handover(input, context);
231
+ const acquired = await native.runOwner.conversationContext(sessionId, input, prepared.context);
232
+ if (acquired.ok === false) return acquired;
233
+ const turn = await prepared.turn(acquired);
234
+ const execution = await native.runOwner.runRenewalHandover(sessionId, turn.input, turn.context);
235
+ return turn.completeResult(execution);
236
+ }
710
237
 
711
- function messagePreparation(configuration, context, signal, tools) {
712
- if (supplied) return tools ? { ...supplied.messagePreparation, applicationTools: tools,
713
- providerReady: current.providerReady } : supplied.messagePreparation;
714
- return {
715
- async readContext() {
716
- const session = await runtime.getSession(sessionId);
717
- return { runtime, session, selection: {
718
- runtime, session, threadId: () => binding.threadId,
719
- async acquireProvider() {
720
- const reused = Boolean(native?.isAvailable());
721
- await connect(signal, await selection(configuration, context, signal));
722
- return { provider: native, reused };
723
- }
724
- } };
725
- },
726
- threadPreparation() {
727
- return {
728
- get failureThreadId() { return binding.threadId; },
729
- async provider() {
730
- const { selected, params, toolSchemaIdentity } = await prepareConfiguration(configuration, context, signal, tools);
731
- const retainedThreadId = binding.threadId;
732
- return {
733
- provider: native,
734
- workdir: binding.workdir,
735
- observerOptions: { providerKey },
736
- requirePaginatedHistory: !retainedThreadId,
737
- settings: { ...selected, resumeOptions: params },
738
- preparation: {
739
- settings: async () => ({ threadSettings: params, threadStartSettings: params }),
740
- identity: { read: () => retainedThreadId,
741
- async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
742
- }
743
- };
744
- }
745
- };
746
- },
747
- async prepareMessage(input, prepared, { starting }) {
748
- if (!starting) return null;
749
- const { settings } = prepared;
750
- return { renderedPrompt: input.message,
751
- get turnSettings() { return { cwd: binding.workdir,
752
- ...(settings.model ? { model: settings.model } : {}), effort: configuration.effort || null,
753
- ...(configuration.outputSchema ? { outputSchema: configuration.outputSchema } : {}) }; }
754
- };
755
- },
756
- async finishMessage(input, prepared, outcome) {
757
- if (Object.hasOwn(outcome, "error")) throw outcome.error;
758
- }
759
- };
760
- }
238
+ async function seedRenewalHandover({ sessionId, native, input, context }) {
239
+ const prepared = native.preparation.seed(input, context);
240
+ const acquired = await native.runOwner.conversationContext(sessionId, input, prepared.context);
241
+ if (acquired.ok === false) return acquired;
242
+ const turn = prepared.turn(acquired);
243
+ const execution = await native.runOwner.runRenewalSeed(turn.input, turn.context);
244
+ return turn.completeResult(execution);
245
+ }
761
246
 
762
- async function acceptMessage(command, nativeIdentity, row, nativeResult) {
763
- if (typeof nativeIdentity?.threadId !== "string" || !nativeIdentity.threadId.trim() ||
764
- typeof nativeIdentity?.turnId !== "string" || !nativeIdentity.turnId.trim()) {
765
- throw Object.assign(new Error("The native delivery has no exact turn identity. Inspect this message's receipt before continuing."), {
766
- code: "conversation_delivery_uncertain", delivery: "uncertain"
767
- });
768
- }
769
- const { turnId } = nativeIdentity;
770
- const active = current;
771
- if (!active) throw new Error("The native delivery lost its active conversation owner.");
772
- active.inputId = command.input.messageId;
773
- if (!active.turnId) active.turnId = turnId;
774
- await command.accept({ nativeTurnId: turnId, conversationTurn: row || await deliveredRow(command.input, nativeIdentity),
775
- ...(supplied ? { nativeResult } : {}) });
776
- active.admitted = true;
777
- active.admission.resolve();
778
- if (active.pendingGoal !== undefined) {
779
- const goal = active.pendingGoal;
780
- delete active.pendingGoal;
781
- await active.onEvent({ type: "goal", goal: goalValue(goal) });
782
- }
783
- if (active.pendingNativeTurn) {
784
- const next = active.pendingNativeTurn;
785
- delete active.pendingNativeTurn;
786
- await active.onNativeTurn({ turnId: next, nativeOwner: true });
787
- }
788
- }
247
+ async function ensureConversation({ sessionId, native }) {
248
+ return native.runOwner.ensureSessionReadiness(sessionId, await native.preparation.readiness());
249
+ }
789
250
 
790
- async function sendMessage(command, configuration, context, signal, tools) {
791
- return withDelivery(command, async () => {
792
- let attempted = false;
793
- let rejected = false;
794
- let accepted = false;
795
- const input = deliveryInput(command.input, async value => {
796
- await command.beforeDispatch(value);
797
- signal.throwIfAborted();
798
- attempted = true;
799
- current.dispatched = true;
800
- }, () => { rejected = true; });
801
- try {
802
- const delivered = await owner.dispatchMessage(sessionId, input, {
803
- ...(supplied ? { ...context, actorContext: input.actorContext || null } : { preparedInput: nativeInput(command.input) })
804
- }, messagePreparation(configuration, context, signal, tools));
805
- const { value, nativeIdentity } = delivered;
806
- if (supplied ? value.delivered === true : value.delivered || nativeIdentity) {
807
- await acceptMessage(command, nativeIdentity || { threadId: value.threadId, turnId: value.turnId }, value.conversationTurn, delivered);
808
- accepted = true;
809
- if (value.ok === false) throw new Error(value.error);
810
- return delivered;
251
+ async function updateConversationSettings({ sessionId, native, context, input }) {
252
+ // Changing a thread's settings is not a use that keeps Codex running.
253
+ const borrow = { release() {} };
254
+ try {
255
+ const acquired = await native.runOwner.conversationContext(sessionId, { conversationId: input.threadId }, context, borrow);
256
+ if (acquired.ok === false) return acquired;
257
+ await acquired.provider.updateThreadSettings(input.threadId, { model: input.model, effort: input.effort });
258
+ return { ok: true };
259
+ } finally {
260
+ borrow.release();
261
+ }
262
+ }
263
+
264
+ /** @param {{ sessionId: string, native: { runOwner: import("../codexRuntimeLifecycle.js").CodexRunOwner }, input: object, context: object }} request */
265
+ async function createConversation({ sessionId, native, input, context }) {
266
+ return native.runOwner.createConversation(sessionId, input, context);
267
+ }
268
+
269
+ async function runDetachedConversation({ sessionId, native, input, options }) {
270
+ return native.runOwner.runDetachedConversation(sessionId, input, options);
271
+ }
272
+
273
+ async function createBinding() {
274
+ assertCodexApplicationEnvironment(host);
275
+ return createInertCodexConversationBinding({ workdir: await realpath(workdir), configRoot });
276
+ }
277
+
278
+ async function open({ binding, writeBinding, onFailure: reportFailure, conversation }) {
279
+ const supplied = conversation?.native;
280
+ if (supplied?.scoped) {
281
+ const { conversationId, context: openingContext } = supplied.scoped;
282
+ const { runOwner: owner } = supplied;
283
+ const sessionId = conversation.sessionId;
284
+ // These are the run owner's scoped commands, including their native
285
+ // start dedup, watcher/deadline and Stop/delete proof. No main journal
286
+ // or additional command queue is introduced for this representation.
287
+ return createScopedConversationHandle({ conversationId,
288
+ sendNative: (input, context) => owner.startConversationTurn(sessionId, input, context),
289
+ readNative: (input, context) => owner.readConversation(sessionId, input, context),
290
+ waitNative: (input, context) => owner.waitForConversationTurn(sessionId, input, context),
291
+ cancel: (input, context) => owner.stopConversation(sessionId, input, context),
292
+ deleteConversation: (input, context) => owner.deleteConversation(sessionId, input, context),
293
+ // Runtime shutdown retains the parent's cleanup receipt. Explicit
294
+ // scoped deletion above owns native deletion and its proof.
295
+ disposeOpening: () => disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options: openingContext })
296
+ });
297
+ }
298
+ if (!supplied) assertCodexApplicationEnvironment(host);
299
+ if (!supplied && (!binding || typeof binding.threadId !== "string" || binding.workdir !== await realpath(workdir) || binding.configRoot !== configRoot)) {
300
+ throw new Error("This Codex conversation belongs to another working directory or credential home. Restore its original host configuration.");
301
+ }
302
+ const session = {
303
+ binding,
304
+ bindingWork: Promise.resolve(),
305
+ native: undefined,
306
+ nativeProviderId: undefined,
307
+ current: undefined,
308
+ stopping: undefined,
309
+ disposed: false,
310
+ preparedThreadSettings: undefined,
311
+ observationError: undefined,
312
+ observationTarget: undefined
313
+ };
314
+ const providerKey = "conversation";
315
+ const providerOwner = supplied?.providerOwner || createCodexAppServerProviderOwner({
316
+ onRecoveryEvent(_owner, event, error) {
317
+ if (event === "stopFailed") session.current?.completion.reject(error);
318
+ }
319
+ });
320
+ if (!conversation?.runtime?.store) throw new TypeError("Codex requires its bound conversation store.");
321
+ const sessionId = conversation.sessionId;
322
+ const nativeStore = conversation.runtime.store;
323
+ const boundedWriter = name => (id, input) => {
324
+ const limit = name === "writeConversationThinkingMessage" ? 4 * 1024 * 1024
325
+ : name.includes("AssistantMessage") ? Math.min(maximumOutput, maximumFinalReply) : maximumOutput;
326
+ const length = input.text?.length || 0;
327
+ if (length <= limit) return nativeStore[name](id, input);
328
+ if (!name.includes("AssistantMessage") || length > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
329
+ return nativeStore.mutateSession(id, async transaction => {
330
+ const { runtime: state } = await transaction.readMetadata();
331
+ const identity = input.nativeIdentity;
332
+ const run = state.binding?.codexAppServerRun;
333
+ if (identity?.threadId && identity.turnId && state.engine === "codex" &&
334
+ state.binding.threadId === identity.threadId && run?.providerThreadId === identity.threadId &&
335
+ run.providerTurnId === identity.turnId && run.outerTurnId) {
336
+ for (const turnId of (await transaction.listTurnIds()).reverse()) {
337
+ const turn = await transaction.readTurn(turnId);
338
+ if (turn?.system?.messageId !== run.outerTurnId) continue;
339
+ if (turn.system.origin === "application" && !turn.system.goal && isCompletedEnvelopeTurn(turn) &&
340
+ turn.metadata.runtime.engine === "codex" && turn.metadata.runtime.segmentId === state.segmentId &&
341
+ turn.metadata.runtime.nativeTurnId === identity.turnId) {
342
+ return nativeStore[name](id, input);
811
343
  }
812
- // The original main sender retains its pending evidence when the
813
- // command returns delivered:false, even if a native ID is known.
814
- if (supplied) return delivered;
815
- throw Object.assign(new Error(value?.error || "Codex is not ready to receive this instruction."), value);
816
- } catch (error) {
817
- if (attempted && !accepted && !rejected && error.delivery !== "not-sent" && !Number.isInteger(error.code)) error.delivery = "uncertain";
818
- throw error;
344
+ break;
819
345
  }
346
+ }
347
+ throw new Error("Codex output exceeded the configured limit.");
348
+ });
349
+ };
350
+ const runtime = supplied ? conversation.runtime : { ...conversation.runtime, store: { ...nativeStore,
351
+ writeConversationAssistantMessage: boundedWriter("writeConversationAssistantMessage"),
352
+ upsertConversationAssistantMessage: boundedWriter("upsertConversationAssistantMessage"),
353
+ writeConversationCommentaryMessage: boundedWriter("writeConversationCommentaryMessage"),
354
+ writeConversationThinkingMessage: boundedWriter("writeConversationThinkingMessage"),
355
+ async updateConversationStream(id, input) {
356
+ if ((input.delta?.length || input.text?.length || 0) > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
357
+ const stream = await nativeStore.updateConversationStream(id, input);
358
+ if (stream?.messages.some(message => message.text.length > maximumOutput)) throw new Error("Codex output exceeded the configured limit.");
359
+ return stream;
360
+ },
361
+ writeConversationUserMessage(id, input) {
362
+ const pending = owner.pendingUserMessages.get(`${sessionId}\0${input.messageId}`);
363
+ return conversation.runtime.store.writeConversationUserMessage(id, {
364
+ ...input, authoredRequest: pending?.authoredInput
820
365
  });
821
366
  }
822
-
823
- async function goalContext(configuration, context, signal, { prepareThread = false, tools } = {}) {
824
- if (supplied) return owner.prepareGoalContext(sessionId, context, supplied.goalPreparation, { prepareThread });
825
- let selected;
826
- if (prepareThread && !binding.threadId) selected = await prepare(configuration, context, signal, tools);
827
- else if (prepareThread) {
828
- const prepared = await prepareConfiguration(configuration, context, signal, tools);
829
- selected = { ...prepared.selected, resumeOptions: prepared.params };
830
- } else {
831
- selected = await selection(configuration, context, signal);
832
- await connect(signal, selected);
367
+ } };
368
+ const owner = supplied?.runOwner || createCodexAppServerRunOwner({
369
+ persistCommentary: conversation.persistCommentary,
370
+ finalizingGraceMs: limits.codexFinalizingGraceMs,
371
+ finalizingGraceAfterHistoryRead: limits.codexFinalizingGraceAfterHistoryRead,
372
+ failureDetailGraceMs: limits.codexFailureDetailGraceMs,
373
+ createRuntime: async () => runtime,
374
+ createStore: async () => runtime.store,
375
+ acquireProvider: async () => session.native,
376
+ hasRuntime: () => Boolean(session.binding.threadId),
377
+ publish: (_id, event) => publishNative(event),
378
+ checkpoint: (_id, input) => conversation.checkpoint({ ...input,
379
+ status: input.turnOutcome === "response_delivery_failure" ? "failed" : input.status }),
380
+ async onNotificationSignal(kind, { threadId, turnId, provider, error }) {
381
+ const active = session.current;
382
+ if (kind !== "provider_error" || !active?.dispatched || active.finished || provider !== session.native ||
383
+ threadId !== session.binding.threadId || !turnId || active.turnId && active.turnId !== turnId) return;
384
+ const tracked = codexAppServerTurnState(await runtime.getSession(sessionId));
385
+ if (tracked.threadId !== threadId || tracked.turnId !== turnId) return;
386
+ // Recovery covers only this admitted turn and keeps native activity
387
+ // truthful; the run's own cleanup owns Stop.
388
+ await owner.recoverActiveTurn(sessionId, { provider, retryOnError: false, providerError: { threadId, turnId, error } });
389
+ if (session.current !== active || active.finished || active.turnId !== turnId ||
390
+ provider !== session.native || threadId !== session.binding.threadId) return;
391
+ const result = await owner.stopTurnWithProviderFailure(sessionId, threadId, turnId, { provider, error, failureDetailReady: true });
392
+ if (session.current === active && active.turnId === turnId && provider === session.native &&
393
+ threadId === session.binding.threadId && result.reason === "provider_still_active") {
394
+ // readable-check: allow no-patched-error - data: status is the turn outcome the conversation reports, not an HTTP status
395
+ active.completion.reject(Object.assign(new Error(error), { status: "failed" }));
833
396
  }
834
- return { runtime, session: await runtime.getSession(sessionId), provider: native,
835
- threadId: binding.threadId, providerKey, resumeOptions: selected.resumeOptions };
836
397
  }
398
+ });
399
+ // Retain this queue's exact opening namespace, never its admission grant.
400
+ const drainNotifications = owner.notificationQueue.captureDrain(sessionId);
401
+ async function publishNative(event) {
402
+ if (supplied) session.binding.threadId = await conversation.identity.read();
403
+ await conversation.publish(event);
404
+ if (event.nativeGoal?.threadId === session.binding.threadId) {
405
+ await recordGoal(event.nativeGoal.goal);
406
+ }
407
+ const turn = event.payload?.agentSession?.turn;
408
+ if (!turn || !session.current?.dispatched) return;
409
+ if (turn.active && session.current.finished && session.current.finishIfCurrent) {
410
+ // The run owner may start the next turn during Send.
411
+ // Its publication replaces only this adapter's settled waiter.
412
+ session.current.completion = Promise.withResolvers();
413
+ session.current.completion.promise.catch(() => {});
414
+ session.current.finished = false;
415
+ }
416
+ if (turn.id && turn.id !== session.current.turnId) {
417
+ const previous = session.current.turnId;
418
+ session.current.turnId = turn.id;
419
+ if (session.current.admitted) {
420
+ if (previous) await session.current.onNativeTurn({ turnId: turn.id, nativeOwner: true });
421
+ } else session.current.pendingNativeTurn = turn.id;
422
+ }
423
+ if (!turn.active && event.reason === "codex-app-server-turn-idle") {
424
+ session.current.finished = true;
425
+ if ((["failed", "interrupted"].includes(turn.status) || !supplied && turn.error) && !session.current.signal.aborted) {
426
+ // readable-check: allow no-patched-error - data: status is the turn outcome the conversation reports, not an HTTP status
427
+ session.current.completion.reject(Object.assign(new Error(turn.error || `Codex turn ${turn.status}.`), {
428
+ status: turn.status === "completed" ? "failed" : turn.status }));
429
+ } else session.current.completion.resolve();
430
+ }
431
+ }
432
+ const savedTurn = codexAppServerTurnState({ agentRuns: session.binding.codexAppServerRun ? [session.binding.codexAppServerRun] : [] });
433
+ session.observationError = session.binding.observationLoss && (!session.binding.observationLoss.stopped ||
434
+ savedTurn.active && savedTurn.status === "observation_lost")
435
+ ? Object.assign(new Error(session.binding.observationLoss.message), { cleanupFailed: true }) : null;
436
+ if (session.observationError) await reportFailure(session.observationError);
437
+ function updateBinding(patch) {
438
+ const work = session.bindingWork.then(async () => {
439
+ const next = { ...session.binding, ...patch };
440
+ await writeBinding(next);
441
+ session.binding = next;
442
+ });
443
+ session.bindingWork = work.catch(() => {});
444
+ return work;
445
+ }
446
+ async function recordGoal(goal) {
447
+ if (supplied) session.binding.goal = goal;
448
+ else await updateBinding({ goal });
449
+ if (session.current?.admitted) await session.current.onEvent({ type: "goal", goal: goalValue(goal) });
450
+ else if (session.current) session.current.pendingGoal = goal;
451
+ else await conversation.publish({ payload: { goal: goalValue(goal) }, reason: "codex-goal" });
452
+ return goal;
453
+ }
837
454
 
838
- async function interrupt({ configuration, context, input = {}, representation } = {}) {
455
+ const connection = createCodexDriverConnection({
456
+ session, supplied, sessionId, providerKey, providerOwner, owner, runtime, reportFailure, updateBinding, recordGoal,
457
+ accountIdentity, connections, env, host, limits, nativeHost, nativeTools
458
+ });
459
+ const { stop, selection, connect } = connection;
460
+ const { goalContext, interrupt, run } = createCodexDriverTurns({
461
+ session, supplied, conversation, sessionId, providerKey, providerOwner, owner, runtime, updateBinding, drainNotifications,
462
+ connection
463
+ });
464
+
465
+ return Object.freeze({
466
+ publishNative,
467
+ async readGoal({ configuration, context }) {
468
+ if (supplied) {
469
+ const target = await supplied.readGoalContext(context);
470
+ const result = target.ok === false ? { status: "unavailable", goal: null }
471
+ : await providerOwner.readSessionGoal(conversation.namespace, target.threadId);
472
+ session.binding.threadId = target.threadId || "";
473
+ session.binding.goal = result.goal || null;
474
+ if (target.completeResult) await target.completeResult(result, {
475
+ segmentId: target.threadId ? `codex:${target.threadId}` : null, capabilities: goalCapabilities
476
+ });
477
+ return { goal: goalValue(result.goal), nativeResult: result };
478
+ }
479
+ if (!session.binding.threadId) return null;
480
+ const signal = AbortSignal.timeout(30_000);
481
+ await connect(signal, await selection(configuration, context, signal));
482
+ const { goal } = await session.native.readGoal(session.binding.threadId, { signal });
483
+ await recordGoal(goal || null);
484
+ return goalValue(goal);
485
+ },
486
+ async updateGoal({ configuration, context, input, tools, representation }) {
839
487
  if (supplied) {
488
+ const invalid = owner.validateGoalInput(input);
489
+ if (invalid) return { goal: goalValue(session.binding.goal), nativeResult: invalid };
840
490
  const admission = supplied.admission();
841
- if (admission.ok === false) {
842
- const value = codexAppServerFrozenTurnInterruptResponse({
843
- threadId: input.threadId || input.codexSessionId, turnId: input.turnId || input.codexTurnId
844
- });
845
- return representation === "native" ? { value } : value;
846
- }
491
+ if (admission.ok === false) return { goal: goalValue(session.binding.goal), nativeResult: admission };
492
+ let nativeContext;
493
+ let result;
847
494
  try {
848
- const prepared = await supplied.controlPreparation(context, input);
849
- if (prepared.ok === false) {
850
- if (representation === "native") return { value: prepared };
851
- throw Object.assign(new Error(prepared.error), prepared, { cleanupFailed: true });
495
+ nativeContext = await goalContext(configuration, context);
496
+ let command = input;
497
+ if (representation !== "native") {
498
+ session.binding.threadId = nativeContext.threadId;
499
+ const goal = session.binding.goal || null;
500
+ if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
501
+ command = { ...input, threadId: session.binding.threadId,
502
+ createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective };
852
503
  }
853
- const result = await owner.interruptTurn(sessionId, input, owner.controlContext(sessionId, prepared));
854
- if (representation === "native") return result;
855
- if (result.value?.ok === false) throw Object.assign(new Error(result.value.error), result.value, { cleanupFailed: true });
856
- return result.value;
504
+ result = await owner.updateGoal(sessionId, command, {
505
+ context: nativeContext, signal: context?.signal, prepareConversation: () => goalContext(configuration, context, undefined, { prepareThread: true })
506
+ });
507
+ if (result.ok !== false) session.binding.goal = result.goal || null;
857
508
  } finally { admission.release(); }
858
- }
859
- if (observationError) {
860
- await stop();
861
- return { ok: true, interrupted: true };
862
- }
863
- const result = await owner.interruptTurn(sessionId, {}, {
864
- runtime, admissionError: () => null, threadId: () => binding.threadId,
865
- observationOwner: () => native,
866
- async acquireProvider() {
867
- if (!native && configuration) {
868
- const signal = AbortSignal.timeout(30_000);
869
- await connect(signal, await selection(configuration, context, signal));
870
- }
871
- if (!native) throw new Error("Codex's retained runtime owner is unavailable.");
872
- await native.ensureAvailable();
873
- return native;
509
+ if (result.ok !== false && input.action === "pause") {
510
+ // Pause keeps the goal and uses the verified Stop owner
511
+ // to stop its current conversation turn as well as scheduling.
512
+ const stopped = await interrupt({ configuration, context });
513
+ if (stopped?.ok === false) throw Object.assign(new Error(stopped.error), stopped, { cleanupFailed: true });
874
514
  }
875
- }).catch(error => {
876
- // A thrown control error also leaves this native owner unconfirmed.
877
- if (error === current?.toolFailure && !current.signal.aborted && !error.cleanupFailed) {
878
- current.toolFailureCleanup = error;
879
- }
880
- error.cleanupFailed = true;
881
- throw error;
515
+ if (nativeContext.completeResult) await nativeContext.completeResult(result);
516
+ return { goal: goalValue(result.goal), nativeResult: result };
517
+ }
518
+ if (session.binding.goalRequest) throw Object.assign(new Error("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal."), {
519
+ code: "conversation_delivery_uncertain"
882
520
  });
883
- if (result.value?.ok === false) throw Object.assign(new Error(result.value.error || "Codex could not confirm that work stopped."), { cleanupFailed: true });
884
- return result.value;
885
- }
886
-
887
- return Object.freeze({
888
- publishNative,
889
- async readGoal({ configuration, context }) {
890
- if (supplied) {
891
- const target = await supplied.readGoalContext(context);
892
- const result = target.ok === false ? { status: "unavailable", goal: null }
893
- : await providerOwner.readSessionGoal(conversation.namespace, target.threadId);
894
- binding.threadId = target.threadId || "";
895
- binding.goal = result.goal || null;
896
- if (target.completeResult) await target.completeResult(result, {
897
- segmentId: target.threadId ? `codex:${target.threadId}` : null, capabilities: goalCapabilities
898
- });
899
- return { goal: goalValue(result.goal), nativeResult: result };
900
- }
901
- if (!binding.threadId) return null;
902
- const signal = AbortSignal.timeout(30_000);
903
- await connect(signal, await selection(configuration, context, signal));
904
- const { goal } = await native.readGoal(binding.threadId, { signal });
905
- await recordGoal(goal || null);
906
- return goalValue(goal);
907
- },
908
- async updateGoal({ configuration, context, input, tools, representation }) {
909
- if (supplied) {
910
- const invalid = owner.validateGoalInput(input);
911
- if (invalid) return { goal: goalValue(binding.goal), nativeResult: invalid };
912
- const admission = supplied.admission();
913
- if (admission.ok === false) return { goal: goalValue(binding.goal), nativeResult: admission };
914
- let nativeContext;
915
- let result;
916
- try {
917
- nativeContext = await goalContext(configuration, context);
918
- let command = input;
919
- if (representation !== "native") {
920
- binding.threadId = nativeContext.threadId;
921
- const goal = binding.goal || null;
922
- if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
923
- command = { ...input, threadId: binding.threadId,
924
- createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective };
925
- }
926
- result = await owner.updateGoal(sessionId, command, {
927
- context: nativeContext, signal: context?.signal, prepareConversation: () => goalContext(configuration, context, undefined, { prepareThread: true })
928
- });
929
- if (result.ok !== false) binding.goal = result.goal || null;
930
- } finally { admission.release(); }
931
- if (result.ok !== false && input.action === "pause") {
932
- // Pause keeps the goal and uses the original verified Stop owner
933
- // to stop its current conversation turn as well as scheduling.
934
- const stopped = await interrupt({ configuration, context });
935
- if (stopped?.ok === false) throw Object.assign(new Error(stopped.error), stopped, { cleanupFailed: true });
936
- }
937
- if (nativeContext.completeResult) await nativeContext.completeResult(result);
938
- return { goal: goalValue(result.goal), nativeResult: result };
939
- }
940
- if (binding.goalRequest) throw Object.assign(new Error("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal."), {
941
- code: "conversation_delivery_uncertain"
942
- });
943
- const signal = AbortSignal.timeout(30_000);
944
- const nativeContext = await goalContext(configuration, context, signal, {
945
- prepareThread: Boolean(binding.threadId && ["set", "resume"].includes(input.action)), tools
946
- });
947
- const goal = binding.threadId ? (await native.readGoal(binding.threadId, { signal })).goal : null;
948
- if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
949
- const result = await owner.updateGoal(sessionId, { ...input, threadId: binding.threadId,
950
- createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective }, {
951
- context: nativeContext, signal: context?.signal,
952
- prepareConversation: () => goalContext(configuration, context, signal, { prepareThread: true, tools })
521
+ const signal = AbortSignal.timeout(30_000);
522
+ const nativeContext = await goalContext(configuration, context, signal, {
523
+ prepareThread: Boolean(session.binding.threadId && ["set", "resume"].includes(input.action)), tools
524
+ });
525
+ const goal = session.binding.threadId ? (await session.native.readGoal(session.binding.threadId, { signal })).goal : null;
526
+ if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
527
+ const result = await owner.updateGoal(sessionId, { ...input, threadId: session.binding.threadId,
528
+ createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective }, {
529
+ context: nativeContext, signal: context?.signal,
530
+ prepareConversation: () => goalContext(configuration, context, signal, { prepareThread: true, tools })
531
+ });
532
+ if (result.ok === false) throw new Error(result.error);
533
+ if (input.action === "pause") await interrupt({ configuration, context });
534
+ return goalValue(result.goal);
535
+ },
536
+ async inspectAdmission({ messageId, threadId, nativeTurnId, configuration, context, goal: command, representation, verifyAccount = false, receiptBinding }) {
537
+ if (supplied) {
538
+ const prepared = await supplied.inspectionPreparation({ messageId, threadId }, context);
539
+ if (prepared.ok === false) return representation === "native" ? prepared : { accepted: false, ...prepared };
540
+ const target = await owner.conversationContext(sessionId, {}, prepared);
541
+ if (target.ok === false) return representation === "native" ? target : { accepted: false, ...target };
542
+ const receipt = await inspectCodexAppServerMessageAdmission({ provider: target.provider, threadId, messageId });
543
+ return representation === "native" ? receipt : { ...receipt, accepted: receipt.admission === "accepted" };
544
+ }
545
+ if (command || session.binding.goalReceipt?.messageId === messageId || session.binding.goalRequest?.messageId === messageId) {
546
+ // Old goal-message records stay inspection-only. Native goal state
547
+ // cannot certify the authored delivery invented by that adapter.
548
+ return { accepted: false, recoveryLimitation: "This older goal-message record requires offline inspection. Its saved history and delivery evidence were preserved." };
549
+ }
550
+ if (verifyAccount === true && (!receiptBinding || threadId !== session.binding.threadId || receiptBinding.threadId !== session.binding.threadId ||
551
+ receiptBinding.workdir !== session.binding.workdir || receiptBinding.configRoot !== session.binding.configRoot)) {
552
+ throw new Error("The completed Codex response belongs to a different native conversation or host scope.");
553
+ }
554
+ if (!session.binding.threadId) return { accepted: false };
555
+ const signal = AbortSignal.timeout(30_000);
556
+ await connect(signal, await selection(configuration, context, signal, verifyAccount === true, receiptBinding));
557
+ let acceptedTurn;
558
+ let cursor;
559
+ do {
560
+ const page = await session.native.client.request("thread/turns/list", { threadId: session.binding.threadId,
561
+ limit: 100, sortDirection: "desc", itemsView: "full", ...(cursor ? { cursor } : {}) }, { signal });
562
+ acceptedTurn = page.data.find(turn => (!nativeTurnId || turn.id === nativeTurnId) &&
563
+ turn.items?.some(item => item.type === "userMessage" && item.clientId === messageId));
564
+ cursor = page.nextCursor;
565
+ } while (!acceptedTurn && cursor);
566
+ if (!acceptedTurn) return { accepted: false };
567
+ const stored = await runtime.getSession(sessionId);
568
+ const tracked = codexAppServerTurnState(stored);
569
+ const before = await runtime.store.readConversationLog(sessionId);
570
+ const request = JSON.parse(await runtime.store.readMetadataValue(sessionId, "assistant_delivery")).engines.codex.pending;
571
+ if (request?.messageId === messageId) await owner.writeDeliveredUserMessage(runtime, sessionId,
572
+ request.displayMessage, messageId, request.turnMetadata, request.displayAttachments,
573
+ { threadId: session.binding.threadId, turnId: acceptedTurn?.id || "" }, request.data);
574
+ let recoveryLimitation;
575
+ if (!stored.agentRuns.length) {
576
+ recoveryLimitation = "Delivery is confirmed, but this older binding has no original native run record. Stored history is preserved; output recovery cannot be proven.";
577
+ } else if (tracked.threadId !== session.binding.threadId || acceptedTurn && tracked.turnId !== acceptedTurn.id && tracked.outerTurnId !== messageId) {
578
+ recoveryLimitation = "Delivery is confirmed. The original native owner tracks a different turn, so this inspection preserves stored history without reconstructing unowned output.";
579
+ } else {
580
+ owner.subscribeEvents(sessionId, session.native, session.binding.threadId, { providerKey });
581
+ await owner.reconcileThreadStatus(sessionId, session.native, session.binding.threadId, {
582
+ source: "delivery_inspection", requireTrackedTurn: true, observeLatestTurn: true
953
583
  });
954
- if (result.ok === false) throw new Error(result.error);
955
- if (input.action === "pause") await interrupt({ configuration, context });
956
- return goalValue(result.goal);
957
- },
958
- async inspectAdmission({ messageId, threadId, nativeTurnId, configuration, context, goal: command, representation, verifyAccount = false, receiptBinding }) {
959
- if (supplied) {
960
- const prepared = await supplied.inspectionPreparation({ messageId, threadId }, context);
961
- if (prepared.ok === false) return representation === "native" ? prepared : { accepted: false, ...prepared };
962
- const target = await owner.conversationContext(sessionId, {}, prepared);
963
- if (target.ok === false) return representation === "native" ? target : { accepted: false, ...target };
964
- const receipt = await inspectCodexAppServerMessageAdmission({ provider: target.provider, threadId, messageId });
965
- return representation === "native" ? receipt : { ...receipt, accepted: receipt.admission === "accepted" };
966
- }
967
- if (command || binding.goalReceipt?.messageId === messageId || binding.goalRequest?.messageId === messageId) {
968
- // Old goal-message records stay inspection-only. Native goal state
969
- // cannot certify the authored delivery invented by that adapter.
970
- return { accepted: false, recoveryLimitation: "This older goal-message record requires offline inspection. Its saved history and delivery evidence were preserved." };
584
+ if (tracked.turnId) {
585
+ const recovered = await owner.submitAssistantResult(sessionId, session.binding.threadId, tracked.turnId, { recoverFromProvider: true });
586
+ if (recovered.reason === "error") throw new Error(recovered.error);
971
587
  }
972
- if (verifyAccount === true && (!receiptBinding || threadId !== binding.threadId || receiptBinding.threadId !== binding.threadId ||
973
- receiptBinding.workdir !== binding.workdir || receiptBinding.configRoot !== binding.configRoot)) {
974
- throw new Error("The completed Codex response belongs to a different native conversation or host scope.");
975
- }
976
- if (!binding.threadId) return { accepted: false };
977
- const signal = AbortSignal.timeout(30_000);
978
- await connect(signal, await selection(configuration, context, signal, verifyAccount === true, receiptBinding));
979
- let acceptedTurn;
980
- let cursor;
981
- do {
982
- const page = await native.client.request("thread/turns/list", { threadId: binding.threadId,
983
- limit: 100, sortDirection: "desc", itemsView: "full", ...(cursor ? { cursor } : {}) }, { signal });
984
- acceptedTurn = page.data.find(turn => (!nativeTurnId || turn.id === nativeTurnId) &&
985
- turn.items?.some(item => item.type === "userMessage" && item.clientId === messageId));
986
- cursor = page.nextCursor;
987
- } while (!acceptedTurn && cursor);
988
- if (!acceptedTurn) return { accepted: false };
989
- const stored = await runtime.getSession(sessionId);
990
- const tracked = codexAppServerTurnState(stored);
991
- const before = await runtime.store.readConversationLog(sessionId);
992
- const request = JSON.parse(await runtime.store.readMetadataValue(sessionId, "assistant_delivery")).engines.codex.pending;
993
- if (request?.messageId === messageId) await owner.writeDeliveredUserMessage(runtime, sessionId,
994
- request.displayMessage, messageId, request.turnMetadata, request.displayAttachments,
995
- { threadId: binding.threadId, turnId: acceptedTurn?.id || "" }, request.data);
996
- let recoveryLimitation;
997
- if (!stored.agentRuns.length) {
998
- recoveryLimitation = "Delivery is confirmed, but this older binding has no original native run record. Stored history is preserved; output recovery cannot be proven.";
999
- } else if (tracked.threadId !== binding.threadId || acceptedTurn && tracked.turnId !== acceptedTurn.id && tracked.outerTurnId !== messageId) {
1000
- recoveryLimitation = "Delivery is confirmed. The original native owner tracks a different turn, so this inspection preserves stored history without reconstructing unowned output.";
1001
- } else {
1002
- owner.subscribeEvents(sessionId, native, binding.threadId, { providerKey });
1003
- await owner.reconcileThreadStatus(sessionId, native, binding.threadId, {
1004
- source: "delivery_inspection", requireTrackedTurn: true, observeLatestTurn: true
1005
- });
1006
- if (tracked.turnId) {
1007
- const recovered = await owner.submitAssistantResult(sessionId, binding.threadId, tracked.turnId, { recoverFromProvider: true });
1008
- if (recovered.reason === "error") throw new Error(recovered.error);
1009
- }
1010
- await owner.notificationQueue.drain(sessionId);
1011
- }
1012
- const after = await runtime.store.readConversationLog(sessionId);
1013
- const row = after.find(turn => (turn.user || turn.system)?.messageId === messageId);
1014
- const latest = codexAppServerTurnState(await runtime.getSession(sessionId));
1015
- return { accepted: true, conversationTurn: row, recovered: JSON.stringify(before) !== JSON.stringify(after),
1016
- nativeTurnId: acceptedTurn?.id || latest.turnId, status: latest.active ? "interrupted" : latest.status,
1017
- ...(recoveryLimitation ? { recoveryLimitation } : {}) };
1018
- },
1019
- steer(command) {
1020
- if (!current || current.finished && !current.finishIfCurrent) throw new Error("Codex has no active turn to steer.");
1021
- return current.steer(command);
1022
- },
1023
- async run({ configuration, context, input, tools, signal, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted,
1024
- onEvent, onNativeTurn, finishIfCurrent }) {
1025
- if (disposed || current) throw new Error("This Codex conversation is closed or already working.");
1026
- const completion = Promise.withResolvers();
1027
- const admission = Promise.withResolvers();
1028
- completion.promise.catch(() => {});
1029
- admission.promise.catch(() => {});
1030
- if (input.goal) throw new Error("Codex goals are controls. Inspect any unresolved goal-message record before continuing.");
1031
- const active = current = { configuration, context, inputId: input.messageId,
1032
- onEvent, onNativeTurn, signal, completion, admission, inputReady: admission.promise, finishIfCurrent,
1033
- admitted: false, dispatched: false, turnId: "" };
1034
- let cancellation;
1035
- let toolFailureRecovery;
1036
- let problem;
1037
- let delivered;
1038
- let waitForTurn = false;
1039
- let executingProvider;
1040
- const toolCalls = new Set();
1041
- const toolRegistrations = new Map();
1042
- // This run still owns the original native provider's current turn.
1043
- const ownsTurn = (provider, threadId, turnId) => current === active && !disposed && !signal.aborted &&
1044
- Boolean(provider) && provider === executingProvider && provider === native &&
1045
- threadId === binding.threadId && turnId === active.turnId;
1046
- function toolHandler(provider, threadId, isRegistered = () => true) {
1047
- return async ({ method, params }) => {
1048
- if (method !== "item/tool/call" || !tools || params?.threadId !== (threadId || binding.threadId) || params.namespace ||
1049
- supplied && (disposed || current !== active || active.finished || signal.aborted || owner.runtimeLifecycle?.closing === true || !isRegistered())) {
1050
- throw new Error("This conversation does not authorize that native tool request.");
1051
- }
1052
- let owned = !supplied;
1053
- const operation = (async () => {
1054
- await active.inputReady;
1055
- if (supplied && owner.runtimeLifecycle?.closing === true) {
1056
- throw new Error("This conversation does not authorize that native tool request.");
1057
- }
1058
- const ownership = Promise.withResolvers();
1059
- owner.notificationQueue.run({ sessionId, provider }, () => {
1060
- if (supplied && (params.turnId !== active.turnId || disposed || current !== active || active.finished ||
1061
- signal.aborted || !isRegistered())) {
1062
- ownership.resolve(false);
1063
- return;
1064
- }
1065
- owned = true;
1066
- const failure = provider.observationFailure;
1067
- if (failure) ownership.reject(failure.cause || failure);
1068
- else ownership.resolve(params.turnId === active.turnId);
1069
- });
1070
- if (!await ownership.promise || supplied && (disposed || current !== active || active.finished || signal.aborted || !isRegistered())) {
1071
- owned = false;
1072
- throw new Error("This application tool call belongs to another turn.");
1073
- }
1074
- let result;
1075
- try {
1076
- result = await tools.execute({ id: params.callId, name: params.tool, arguments: JSON.stringify(params.arguments) });
1077
- } catch (error) {
1078
- if (!supplied && owned && ownsTurn(provider, params.threadId, params.turnId)) active.toolFailure ||= error;
1079
- throw error;
1080
- }
1081
- return { success: result.ok, contentItems: [{ type: "inputText", text: JSON.stringify(result) }] };
1082
- })();
1083
- toolCalls.add(operation);
1084
- try { return await operation; }
1085
- catch (error) {
1086
- if (!supplied || owned && current === active && params.turnId === active.turnId && isRegistered()) {
1087
- if (supplied) active.toolFailure = error;
1088
- active.completion.reject(error);
1089
- }
1090
- throw error;
1091
- }
1092
- finally { toolCalls.delete(operation); }
1093
- };
1094
- }
1095
- active.providerReady = async ({ provider, threadId }) => {
1096
- signal.throwIfAborted();
1097
- if (!threadId) return;
1098
- if (typeof conversation.identity?.readToolSchemaIdentity !== "function") {
1099
- throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
1100
- }
1101
- if (await conversation.identity.read() !== threadId) throw new Error("The native application tool thread changed before dispatch.");
1102
- assertCodexApplicationToolSchemaIdentity(threadId, await conversation.identity.readToolSchemaIdentity(),
1103
- codexApplicationToolConfiguration(tools).toolSchemaIdentity);
1104
- signal.throwIfAborted();
1105
- if (disposed || current !== active) throw new Error("This conversation handle is closed.");
1106
- for (const [previous, owned] of toolRegistrations) {
1107
- if (previous === provider && owned.threadId === threadId && owned.client === provider.client && owned.registration.isCurrent()) return;
1108
- owned.registration.release();
1109
- toolRegistrations.delete(previous);
1110
- }
1111
- const client = provider.client;
1112
- let registration;
1113
- registration = provider.registerThreadRequestHandler(threadId, toolHandler(provider, threadId,
1114
- () => registration?.isCurrent() === true && provider.client === client));
1115
- toolRegistrations.set(provider, { threadId, client, registration });
1116
- };
1117
- function abort() {
1118
- if (cancellation) return;
1119
- cancellation = interrupt();
1120
- cancellation.then(() => active.completion.reject(signal.reason), error => active.completion.reject(error));
1121
- }
1122
- async function submit(command, operation) {
1123
- await admission.promise;
1124
- const ready = Promise.withResolvers();
1125
- const previous = active.inputReady;
1126
- active.inputReady = ready.promise;
1127
- try { return await operation(command, configuration, context, signal, tools); }
1128
- finally { ready.resolve(); active.inputReady = previous; }
1129
- }
1130
- active.steer = command => submit(command, sendMessage);
1131
- signal.addEventListener("abort", abort, { once: true });
1132
- try {
1133
- signal.throwIfAborted();
1134
- // Preparation retains the provider's authenticated account and the
1135
- // stable application-tool entry points. The original owner alone
1136
- // subscribes to notifications and handles native turn state/output.
1137
- if (!supplied) {
1138
- const selected = await selection(configuration, context, signal);
1139
- await connect(signal, selected);
1140
- executingProvider = native;
1141
- }
1142
- if (!supplied) native.setServerRequestHandler(toolHandler(executingProvider));
1143
- const command = { input, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted };
1144
- delivered = await sendMessage(command, configuration, context, signal, tools);
1145
- waitForTurn = !supplied || delivered.value?.delivered === true;
1146
- if (!finishIfCurrent && waitForTurn) {
1147
- await completion.promise;
1148
- await drainNotifications();
1149
- }
1150
- } catch (error) { problem = error; }
1151
- if (finishIfCurrent) {
1152
- try {
1153
- for (;;) {
1154
- const settling = active.completion;
1155
- if (waitForTurn) {
1156
- try { await settling.promise; }
1157
- catch (error) { problem = error; }
1158
- }
1159
- if (await finishIfCurrent(() => active.completion === settling)) break;
1160
- // The same original dispatch established another native turn.
1161
- // Keep its completion and Stop owner until that turn settles.
1162
- problem = undefined;
1163
- waitForTurn = true;
1164
- }
1165
- if (!problem && waitForTurn) await drainNotifications();
1166
- } catch (error) { problem = error; }
1167
- }
1168
- signal.removeEventListener("abort", abort);
1169
- admission.reject(problem || signal.reason);
1170
- const failureThreadId = binding.threadId;
1171
- const failureTurnId = active.turnId;
588
+ await owner.notificationQueue.drain(sessionId);
589
+ }
590
+ const after = await runtime.store.readConversationLog(sessionId);
591
+ const row = after.find(turn => (turn.user || turn.system)?.messageId === messageId);
592
+ const latest = codexAppServerTurnState(await runtime.getSession(sessionId));
593
+ return { accepted: true, conversationTurn: row, recovered: JSON.stringify(before) !== JSON.stringify(after),
594
+ nativeTurnId: acceptedTurn?.id || latest.turnId, status: latest.active ? "interrupted" : latest.status,
595
+ ...(recoveryLimitation ? { recoveryLimitation } : {}) };
596
+ },
597
+ steer(command) {
598
+ if (!session.current || session.current.finished && !session.current.finishIfCurrent) throw new Error("Codex has no active turn to steer.");
599
+ return session.current.steer(command);
600
+ },
601
+ run,
602
+ cancel: interrupt,
603
+ async dispose(options) {
604
+ if (supplied) {
1172
605
  try {
1173
- // Account invalidation already owns this stop attempt. Its failed
1174
- // proof stays available for explicit cleanup through the same owner.
1175
- if ((!supplied && problem || supplied && active.toolFailure || signal.aborted) && problem?.code !== "codex_runtime_invalidated") {
1176
- await (cancellation || interrupt());
1177
- }
606
+ const result = await disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options });
607
+ if (result?.ok === false) throw Object.assign(new Error(result.error), result);
608
+ session.disposed = true;
609
+ // readable-check: allow no-patched-error - data: status is the turn outcome the conversation reports, not an HTTP status
610
+ session.current?.completion.reject(Object.assign(new Error("This conversation handle is closed."), { status: "interrupted" }));
611
+ return result;
1178
612
  } catch (error) {
1179
- problem = error;
1180
- // A direct Stop checkpoint can throw before its native completion
1181
- // reaches the queue. Keep the same admitted tool failure with the
1182
- // original observation-stop owner until its pending recovery joins.
1183
- if (!supplied && error === active.toolFailure && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
1184
- error.code !== "codex_runtime_invalidated") {
1185
- toolFailureRecovery = executingProvider.failObservation(error);
1186
- }
1187
- }
1188
- try {
1189
- await drainNotifications();
1190
- // The original queue starts observation recovery without awaiting
1191
- // it. Join only this provider's existing attempt before settling.
1192
- if (!supplied) await providerOwner.pendingRecovery(providerKey, executingProvider);
1193
- if (toolFailureRecovery) {
1194
- await toolFailureRecovery;
1195
- if (problem === active.toolFailureCleanup && ownsTurn(executingProvider, failureThreadId, failureTurnId) &&
1196
- !observationError && binding.observationLoss?.stopped === true) delete problem.cleanupFailed;
1197
- }
1198
- } catch (error) { problem = error; }
1199
- finally {
1200
- for (const owned of toolRegistrations.values()) owned.registration.release();
1201
- toolRegistrations.clear();
1202
- await Promise.allSettled([...toolCalls]);
1203
- if (!supplied) {
1204
- native?.setServerRequestHandler(null);
1205
- owner.clearSessionRecoveryTimers(sessionId);
1206
- }
1207
- current = null;
1208
- }
1209
- if (problem) throw problem;
1210
- return delivered;
1211
- },
1212
- cancel: interrupt,
1213
- async dispose(options) {
1214
- if (supplied) {
1215
- try {
1216
- const result = await disposeNative({ sessionId, namespace: conversation.namespace, native: supplied, options });
1217
- if (result?.ok === false) throw Object.assign(new Error(result.error), result);
1218
- disposed = true;
1219
- current?.completion.reject(Object.assign(new Error("This conversation handle is closed."), { status: "interrupted" }));
1220
- return result;
1221
- } catch (error) {
1222
- error.cleanupFailed = true;
1223
- current?.completion.reject(error);
1224
- throw error;
1225
- }
613
+ error.cleanupFailed = true;
614
+ session.current?.completion.reject(error);
615
+ throw error;
1226
616
  }
1227
- await stop();
1228
- owner.clearSessionRecoveryTimers(sessionId);
1229
- for (const key of owner.eventSubscriptions.keys()) owner.unsubscribeEventSubscription(key);
1230
- await owner.notificationQueue.drain(sessionId);
1231
- await providerOwner.stopCachedProvider(providerKey, { preserveProcessExitProof: true, requireStopped: true });
1232
- disposed = true;
1233
617
  }
1234
- });
1235
- }
618
+ await stop();
619
+ owner.clearSessionRecoveryTimers(sessionId);
620
+ for (const key of owner.eventSubscriptions.keys()) owner.unsubscribeEventSubscription(key);
621
+ await owner.notificationQueue.drain(sessionId);
622
+ await providerOwner.stopCachedProvider(providerKey, { preserveProcessExitProof: true, requireStopped: true });
623
+ session.disposed = true;
624
+ }
625
+ });
626
+ }
627
+
628
+ return Object.freeze({
629
+ // Codex fixes dynamic tool definitions when the thread is created. The three
630
+ // discovery entry points stay stable while the authorized action set changes.
631
+ toolDiscovery: true,
632
+ canonicalTranscript: true,
633
+ attachmentTypes: ["text", "image", "localFile"],
634
+ capabilities: Object.freeze({ streaming: true, cancellation: true, history: true,
635
+ instructions: true, configuration: true, steering: true, ...goalCapabilities,
636
+ attachments: true, tools: true, nativeTools, structuredOutput: true }),
637
+ validateConfiguration,
638
+ disposeNative,
639
+ interruptDetachedConversation,
640
+ deleteDetachedConversation,
641
+ listConversationStorage,
642
+ retireConversationHistory,
643
+ invalidateRuntimes,
644
+ reconcileSessions,
645
+ unsubscribeSessions,
646
+ closeProject,
647
+ inspectTemporaryActivity,
648
+ releaseRenewalPredecessorProcessExitProof,
649
+ releaseRenewalSuccessorProcessExitProof,
650
+ generateRenewalHandover,
651
+ seedRenewalHandover,
652
+ ensureConversation,
653
+ updateConversationSettings,
654
+ createConversation,
655
+ runDetachedConversation,
656
+ createBinding,
657
+ open
1236
658
  });
1237
659
  }