@jskit-ai/assistant-core 0.1.199 → 0.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,1559 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import WebSocket from "ws";
3
+ import { spawn } from "node:child_process";
4
+ import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import { CodexAppServerJsonRpcClient, socketPathFromCodexAppServerEndpoint } from "./codexClient.js";
10
+ import { createConversationProcessIdentity } from "./localExecution.js";
11
+ import { isPlainObject, normalizeText } from "./normalize.js";
12
+ import { prepareCodexModelCatalog } from "./codexConfiguration.js";
13
+ import { startCodexHistoryAdapter } from "./codexHistoryAdapter.js";
14
+
15
+ const CODEX_APP_SERVER_METADATA_FILE = "runtime.json";
16
+ const CODEX_APP_SERVER_LOCK_DIR = "runtime.lock";
17
+ const CODEX_APP_SERVER_LOCK_TIMEOUT_MS = 10000;
18
+ const CODEX_APP_SERVER_LOCK_STALE_MS = 120000;
19
+ const CODEX_APP_SERVER_EXECUTION_MODES = Object.freeze({ HELPER: "helper", INTERACTIVE: "interactive" });
20
+ const runtimeProcessIdentity = createConversationProcessIdentity();
21
+
22
+ // Run in the host-owned execution's leader, independently of subscribers.
23
+ export async function runCodexAppServerProcess({
24
+ runtimeDir, command, args = [], runtimeToken: token, bundled = false, additionalModels = []
25
+ }) {
26
+ const descriptor = path.join(runtimeDir, "history-adapter.json");
27
+ const startup = new AbortController();
28
+ const abortStartup = () => startup.abort();
29
+ process.once("SIGTERM", abortStartup);
30
+ process.once("SIGINT", abortStartup);
31
+ const catalogPath = await prepareCodexModelCatalog({
32
+ command,
33
+ runtimeDir,
34
+ bundled,
35
+ additionalModels,
36
+ signal: startup.signal
37
+ });
38
+ const adapter = await startCodexHistoryAdapter({ token, codexHome: process.env.CODEX_HOME || path.join(os.homedir(), ".codex") });
39
+ await writeFile(`${descriptor}.tmp`, JSON.stringify({ baseUrl: adapter.baseUrl, runtimeToken: token }), { mode: 0o600 });
40
+ await rename(`${descriptor}.tmp`, descriptor);
41
+ const child = spawn(command, [...args, "-c", `model_catalog_json=${JSON.stringify(catalogPath)}`], { stdio: "inherit" });
42
+ let stopping = false;
43
+ let forceExit;
44
+ function stop(signal = "SIGTERM") {
45
+ if (stopping) return;
46
+ stopping = true;
47
+ void adapter.close();
48
+ child.kill(signal);
49
+ forceExit = setTimeout(() => child.kill("SIGKILL"), 5000);
50
+ forceExit.unref();
51
+ }
52
+ process.on("SIGTERM", () => stop());
53
+ process.on("SIGINT", () => stop("SIGINT"));
54
+ process.removeListener("SIGTERM", abortStartup);
55
+ process.removeListener("SIGINT", abortStartup);
56
+ if (startup.signal.aborted) stop();
57
+ adapter.server.on("error", () => { process.exitCode = 1; stop(); });
58
+ const result = await new Promise((resolve) => {
59
+ child.once("error", () => resolve({ code: 1 }));
60
+ child.once("exit", (code, signal) => resolve({ code, signal }));
61
+ });
62
+ clearTimeout(forceExit);
63
+ await adapter.close();
64
+ await rm(descriptor, { force: true });
65
+ process.exitCode ||= result.code ?? (stopping ? 0 : 1);
66
+ }
67
+
68
+ async function ensurePrivateDirectory(dirPath = "") {
69
+ await mkdir(dirPath, {
70
+ mode: 0o700,
71
+ recursive: true
72
+ });
73
+ await chmod(dirPath, 0o700).catch(() => null);
74
+ }
75
+
76
+ export async function ensureWritablePrivateDirectory(dirPath = "") {
77
+ await ensurePrivateDirectory(dirPath);
78
+ const probePath = path.join(dirPath, `.jskit-write-check-${process.pid}-${randomUUID()}`);
79
+ try {
80
+ await writeFile(probePath, "", {
81
+ mode: 0o600
82
+ });
83
+ } catch (error) {
84
+ throw new Error(
85
+ `Codex app-server runtime directory is not writable: ${dirPath}. ${String(error?.message || error)}`
86
+ );
87
+ } finally {
88
+ await rm(probePath, {
89
+ force: true
90
+ }).catch(() => null);
91
+ }
92
+ }
93
+
94
+ export function codexAppServerMetadataPath(runtimeDir = "") {
95
+ return path.join(runtimeDir, CODEX_APP_SERVER_METADATA_FILE);
96
+ }
97
+
98
+ function codexAppServerLockDir(runtimeDir = "") {
99
+ return path.join(runtimeDir, CODEX_APP_SERVER_LOCK_DIR);
100
+ }
101
+
102
+ export function normalizeCodexAppServerMetadata(metadata = {}) {
103
+ const normalized = isPlainObject(metadata) ? metadata : {};
104
+ const endpoint = normalizeText(normalized.endpoint);
105
+ return {
106
+ accountIdentitySignature: normalizeText(normalized.accountIdentitySignature),
107
+ attachmentHostRoot: normalizeText(normalized.attachmentHostRoot),
108
+ authStateSignature: normalizeText(normalized.authStateSignature),
109
+ endpoint,
110
+ executionId: normalizeText(normalized.executionId),
111
+ executionMode: normalizeText(normalized.executionMode) || CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
112
+ executionContextHash: normalizeText(normalized.executionContextHash),
113
+ healthz: normalizeText(normalized.healthz),
114
+ historyAdapterBaseUrl: normalizeText(normalized.historyAdapterBaseUrl),
115
+ logPath: normalizeText(normalized.logPath),
116
+ pid: Number.isSafeInteger(Number(normalized.pid)) ? Number(normalized.pid) : null,
117
+ processCwd: normalizeText(normalized.processCwd),
118
+ processExitVerifiedAt: normalizeText(normalized.processExitVerifiedAt),
119
+ processIdentity: runtimeProcessIdentity.normalize(normalized.processIdentity),
120
+ processState: normalizeText(normalized.processState),
121
+ provider: normalizeText(normalized.provider),
122
+ readyz: normalizeText(normalized.readyz),
123
+ runtimeDir: normalizeText(normalized.runtimeDir),
124
+ runtimesHash: normalizeText(normalized.runtimesHash),
125
+ schemaVersion: Number(normalized.schemaVersion || 0),
126
+ socketPath: normalizeText(normalized.socketPath),
127
+ startedAt: normalizeText(normalized.startedAt),
128
+ terminalEnvHash: normalizeText(normalized.terminalEnvHash),
129
+ toolHomeSource: normalizeText(normalized.toolHomeSource),
130
+ transport: normalizeText(normalized.transport)
131
+ };
132
+ }
133
+
134
+ export async function readCodexAppServerMetadata(runtimeDir = "") {
135
+ try {
136
+ const metadata = JSON.parse(await readFile(codexAppServerMetadataPath(runtimeDir), "utf8"));
137
+ return normalizeCodexAppServerMetadata(metadata);
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ export async function writeCodexAppServerMetadata(runtimeDir = "", metadata = {}) {
144
+ await ensureWritablePrivateDirectory(runtimeDir);
145
+ const metadataPath = codexAppServerMetadataPath(runtimeDir);
146
+ const tempPath = `${metadataPath}.${process.pid}.${randomUUID()}.tmp`;
147
+ await writeFile(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, {
148
+ mode: 0o600
149
+ });
150
+ await chmod(tempPath, 0o600).catch(() => null);
151
+ await rename(tempPath, metadataPath);
152
+ await chmod(metadataPath, 0o600).catch(() => null);
153
+ }
154
+
155
+ export async function fileExists(filePath = "") {
156
+ try {
157
+ const entry = await stat(filePath);
158
+ return entry.isSocket() || entry.isFile();
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ export async function tailTextFile(filePath = "", maxBytes = 4096) {
165
+ try {
166
+ const text = await readFile(filePath, "utf8");
167
+ return text.slice(-maxBytes);
168
+ } catch {
169
+ return "";
170
+ }
171
+ }
172
+
173
+ async function readLockOwner(lockDir = "") {
174
+ try {
175
+ return JSON.parse(await readFile(path.join(lockDir, "owner.json"), "utf8"));
176
+ } catch {
177
+ return {};
178
+ }
179
+ }
180
+
181
+ async function lockIsStale(lockDir = "") {
182
+ const owner = await readLockOwner(lockDir);
183
+ if (owner.pid && runtimeProcessIdentity.isProcessAlive(owner.pid)) {
184
+ const createdAtMs = Date.parse(owner.createdAt || "");
185
+ return Number.isFinite(createdAtMs) && Date.now() - createdAtMs > CODEX_APP_SERVER_LOCK_STALE_MS;
186
+ }
187
+ return true;
188
+ }
189
+
190
+ export async function acquireCodexRuntimeLock(runtimeDir = "", {
191
+ busyCode = "codex_runtime_busy",
192
+ timeoutMs = CODEX_APP_SERVER_LOCK_TIMEOUT_MS
193
+ } = {}) {
194
+ await ensureWritablePrivateDirectory(runtimeDir);
195
+ const lockDir = codexAppServerLockDir(runtimeDir);
196
+ const startedAt = Date.now();
197
+ while (Date.now() - startedAt <= timeoutMs) {
198
+ try {
199
+ await mkdir(lockDir, {
200
+ mode: 0o700
201
+ });
202
+ await writeFile(path.join(lockDir, "owner.json"), `${JSON.stringify({
203
+ createdAt: new Date().toISOString(),
204
+ pid: process.pid
205
+ })}\n`, {
206
+ mode: 0o600
207
+ });
208
+ return async () => {
209
+ await rm(lockDir, {
210
+ force: true,
211
+ recursive: true
212
+ });
213
+ };
214
+ } catch (error) {
215
+ if (error?.code !== "EEXIST") {
216
+ throw error;
217
+ }
218
+ if (await lockIsStale(lockDir)) {
219
+ await rm(lockDir, {
220
+ force: true,
221
+ recursive: true
222
+ });
223
+ continue;
224
+ }
225
+ await delay(100);
226
+ }
227
+ }
228
+ const error = new Error("Codex is still reconnecting: its shared runtime is busy starting or stopping. Please try again shortly.");
229
+ error.code = busyCode;
230
+ error.retryable = true;
231
+ throw error;
232
+ }
233
+
234
+ export const CODEX_APP_SERVER_METADATA_SCHEMA_VERSION = 19;
235
+
236
+ const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
237
+
238
+ export const CODEX_APP_SERVER_TRANSPORT = Object.freeze({
239
+ UNIX: "unix"
240
+ });
241
+
242
+ export const CODEX_APP_SERVER_RUNTIME_DIR_NAME = "codex-app-server";
243
+
244
+ const CODEX_APP_SERVER_LOG_FILE = "app-server.log";
245
+
246
+ const CODEX_APP_SERVER_SOCKET_FILE = "app-server.sock";
247
+
248
+ const CODEX_APP_SERVER_READY_TIMEOUT_MS = 60000;
249
+
250
+ const CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS = 2000;
251
+
252
+ const CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES = new Set([
253
+ "DBUS_SESSION_BUS_ADDRESS",
254
+ "DBUS_STARTER_ADDRESS",
255
+ "DBUS_STARTER_BUS_TYPE"
256
+ ]);
257
+
258
+ const CODEX_APP_SERVER_HELPER_HOME_DIR = "codex-home";
259
+
260
+ const CODEX_APP_SERVER_HELPER_WORKSPACE_DIR = "workspace";
261
+
262
+ const CODEX_APP_SERVER_UNIX_SOCKET_PATH_MAX_BYTES = process.platform === "linux" ? 107 : 103;
263
+
264
+ const CODEX_APP_SERVER_ENDPOINT_STATUS = Object.freeze({
265
+ MISSING: "missing",
266
+ RESPONSIVE: "responsive",
267
+ TIMEOUT: "timeout",
268
+ UNREACHABLE: "unreachable"
269
+ });
270
+
271
+ const CODEX_APP_SERVER_RUNTIME_STATUS = Object.freeze({
272
+ EXITED: "exited",
273
+ INCOMPATIBLE: "incompatible",
274
+ LIVE: "live",
275
+ MISSING: "missing",
276
+ SUSPECT: "suspect"
277
+ });
278
+
279
+ export const CODEX_APP_SERVER_PROCESS_STATE = Object.freeze({
280
+ RUNNING: "running",
281
+ STOPPED: "stopped"
282
+ });
283
+
284
+ const CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS = Object.freeze({
285
+ ABSENT: "absent",
286
+ AMBIGUOUS: "ambiguous",
287
+ EXACT: "exact",
288
+ INVALID: "invalid",
289
+ MISMATCH: "mismatch"
290
+ });
291
+
292
+ function normalizePositiveInteger(value, fallback) {
293
+ const number = Number(value);
294
+ return Number.isSafeInteger(number) && number > 0 ? number : fallback;
295
+ }
296
+
297
+ function stableHash(value) {
298
+ return createHash("sha256")
299
+ .update(String(value || ""))
300
+ .digest("hex")
301
+ .slice(0, 12);
302
+ }
303
+
304
+ export function codexAppServerExecutionMode(options = {}) {
305
+ return normalizeText(options.executionMode) === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
306
+ ? CODEX_APP_SERVER_EXECUTION_MODES.HELPER
307
+ : CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE;
308
+ }
309
+
310
+ export function codexAppServerIsHelper(options = {}) {
311
+ return codexAppServerExecutionMode(options) === CODEX_APP_SERVER_EXECUTION_MODES.HELPER;
312
+ }
313
+
314
+ export async function assertExistingDirectory(dirPath = "", label = "directory") {
315
+ const normalizedPath = normalizeText(dirPath);
316
+ if (!normalizedPath) {
317
+ return;
318
+ }
319
+ const stats = await stat(normalizedPath);
320
+ if (!stats.isDirectory()) {
321
+ throw new Error(`${label} is not a directory: ${normalizedPath}`);
322
+ }
323
+ }
324
+
325
+ export function codexAppServerHelperHomeDir(runtimeDir = "") {
326
+ return path.join(runtimeDir, CODEX_APP_SERVER_HELPER_HOME_DIR);
327
+ }
328
+
329
+ export function codexAppServerHelperWorkspaceDir(runtimeDir = "") {
330
+ return path.join(runtimeDir, CODEX_APP_SERVER_HELPER_WORKSPACE_DIR);
331
+ }
332
+
333
+ function codexAppServerHelperCommandBaseEnv(env = process.env, codexHome = "") {
334
+ const source = isPlainObject(env) ? env : {};
335
+ const cleared = Object.fromEntries(Object.keys({
336
+ ...process.env,
337
+ ...source
338
+ }).map((name) => [name, ""]));
339
+ const allowedNames = [
340
+ "LANG",
341
+ "LC_ALL",
342
+ "LC_CTYPE",
343
+ "PATH",
344
+ "SSL_CERT_DIR",
345
+ "SSL_CERT_FILE",
346
+ "TERM",
347
+ "TMPDIR",
348
+ "TZ"
349
+ ];
350
+ return {
351
+ ...cleared,
352
+ ...Object.fromEntries(allowedNames
353
+ .filter((name) => typeof source[name] === "string" && source[name])
354
+ .map((name) => [name, source[name]])),
355
+ CODEX_HOME: codexHome
356
+ };
357
+ }
358
+
359
+ function codexAppServerLogPath(runtimeDir = "") {
360
+ return path.join(runtimeDir, CODEX_APP_SERVER_LOG_FILE);
361
+ }
362
+
363
+ function codexAppServerSocketPath(runtimeDir = "") {
364
+ return path.join(runtimeDir, CODEX_APP_SERVER_SOCKET_FILE);
365
+ }
366
+
367
+ function codexAppServerSocketPathBytes(socketPath = "") {
368
+ return Buffer.byteLength(String(socketPath || ""), "utf8");
369
+ }
370
+
371
+ function codexAppServerSocketPathTooLong(socketPath = "") {
372
+ return codexAppServerSocketPathBytes(socketPath) > CODEX_APP_SERVER_UNIX_SOCKET_PATH_MAX_BYTES;
373
+ }
374
+
375
+ function codexAppServerUnixEndpoint(socketPath = "") {
376
+ return `unix://${socketPath}`;
377
+ }
378
+
379
+ function codexAppServerProjectTrustOverride(workdir = "") {
380
+ const normalizedWorkdir = normalizeText(workdir);
381
+ if (!normalizedWorkdir) {
382
+ return "";
383
+ }
384
+ const projectRoot = path.resolve(normalizedWorkdir);
385
+ return `projects={${JSON.stringify(projectRoot)}={trust_level="trusted"}}`;
386
+ }
387
+
388
+ export function codexAppServerProcessCwd({
389
+ executionRoot = "",
390
+ runtimeDir = "",
391
+ workdir = ""
392
+ } = {}) {
393
+ const normalizedWorkdir = normalizeText(workdir) ? path.resolve(workdir) : "";
394
+ if (normalizedWorkdir) {
395
+ return normalizedWorkdir;
396
+ }
397
+ const normalizedExecutionRoot = normalizeText(executionRoot) ? path.resolve(executionRoot) : "";
398
+ if (normalizedExecutionRoot) {
399
+ return normalizedExecutionRoot;
400
+ }
401
+ return normalizeText(runtimeDir) ? path.resolve(runtimeDir) : "";
402
+ }
403
+
404
+ function codexAppServerRuntimeDirIsManaged(runtimeDir = "") {
405
+ const normalizedRuntimeDir = normalizeText(runtimeDir);
406
+ if (!normalizedRuntimeDir) {
407
+ return false;
408
+ }
409
+ const basename = path.basename(path.resolve(normalizedRuntimeDir));
410
+ return basename === CODEX_APP_SERVER_RUNTIME_DIR_NAME ||
411
+ basename.startsWith(`${CODEX_APP_SERVER_RUNTIME_DIR_NAME}-`);
412
+ }
413
+
414
+ async function removeCodexAppServerRuntimeDir(runtimeDir = "") {
415
+ const normalizedRuntimeDir = normalizeText(runtimeDir);
416
+ if (!codexAppServerRuntimeDirIsManaged(normalizedRuntimeDir)) {
417
+ return false;
418
+ }
419
+ await rm(normalizedRuntimeDir, {
420
+ force: true,
421
+ recursive: true
422
+ });
423
+ return true;
424
+ }
425
+
426
+ async function removeCodexAppServerMetadataTemps(runtimeDir = "") {
427
+ let names = [];
428
+ try {
429
+ names = await readdir(runtimeDir);
430
+ } catch {
431
+ return;
432
+ }
433
+ await Promise.all(names
434
+ .filter((name) => name.startsWith(`${CODEX_APP_SERVER_METADATA_FILE}.`) && name.endsWith(".tmp"))
435
+ .map((name) => rm(path.join(runtimeDir, name), { force: true })));
436
+ }
437
+
438
+ function codexAppServerRuntimeCleanupCanSkip(error) {
439
+ return ["EACCES", "EPERM", "ENOENT"].includes(String(error?.code || ""));
440
+ }
441
+
442
+ export function codexAppServerRuntimeIdentity(runtime = {}) {
443
+ return [
444
+ normalizeText(runtime.accountIdentitySignature),
445
+ normalizeText(runtime.authStateSignature),
446
+ normalizeText(runtime.endpoint),
447
+ normalizeText(runtime.executionId),
448
+ normalizeText(runtime.executionMode),
449
+ normalizeText(runtime.runtimesHash),
450
+ normalizeText(runtime.terminalEnvHash),
451
+ normalizeText(runtime.socketPath),
452
+ normalizeText(runtime.startedAt),
453
+ normalizeText(runtime.pid)
454
+ ].join("\0");
455
+ }
456
+
457
+ export function normalizeCodexAppServerTerminalEnv(terminalEnv = {}) {
458
+ if (!isPlainObject(terminalEnv)) {
459
+ return {};
460
+ }
461
+ return Object.fromEntries(Object.entries(terminalEnv)
462
+ .map(([name, value]) => [
463
+ normalizeText(name),
464
+ String(value ?? "")
465
+ ])
466
+ .filter(([name, value]) => (
467
+ name &&
468
+ String(value || "") &&
469
+ !CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES.has(name)
470
+ )));
471
+ }
472
+
473
+ export function codexAppServerCommandBaseEnv({
474
+ env = process.env,
475
+ terminalEnv = {}
476
+ } = {}) {
477
+ const baseEnv = {
478
+ ...env,
479
+ ...normalizeCodexAppServerTerminalEnv(terminalEnv)
480
+ };
481
+ for (const name of CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES) {
482
+ delete baseEnv[name];
483
+ }
484
+ return baseEnv;
485
+ }
486
+
487
+ export function codexAppServerCredentialHome(toolHomeSource = "", baseEnv = {}) {
488
+ const home = normalizeText(toolHomeSource);
489
+ if (!home) {
490
+ return {};
491
+ }
492
+ return {
493
+ home,
494
+ username: normalizeText(baseEnv.USER || baseEnv.LOGNAME)
495
+ };
496
+ }
497
+
498
+ export function codexAppServerTerminalEnvHash(terminalEnv = {}) {
499
+ return stableHash(JSON.stringify(Object.entries(normalizeCodexAppServerTerminalEnv(terminalEnv))
500
+ .sort(([left], [right]) => left.localeCompare(right))));
501
+ }
502
+
503
+ function codexHistoryAdapterUrlIsValid(baseUrl, token) {
504
+ // The previous adapter cannot serve thread-bound compaction recovery. Keep
505
+ // its process identity readable, but retire it through normal owned cleanup.
506
+ const match = typeof baseUrl === "string" && /^http:\/\/127\.0\.0\.1:([1-9]\d{0,4})\/[0-9a-f-]{36}\/v2$/iu.exec(baseUrl);
507
+ return Boolean(match && Number(match[1]) <= 65535 && baseUrl.endsWith(`/${token}/v2`));
508
+ }
509
+
510
+ function codexAppServerLivenessTimeoutMs(options = {}) {
511
+ return normalizePositiveInteger(
512
+ options.livenessTimeoutMs,
513
+ normalizePositiveInteger(options.timeoutMs, CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS)
514
+ );
515
+ }
516
+
517
+ function codexAppServerProcessCommandHash({
518
+ codexArgs = [],
519
+ codexCommand = "",
520
+ executionMode = "",
521
+ processCwd = "",
522
+ runtimeDir = ""
523
+ } = {}) {
524
+ return stableHash(JSON.stringify({
525
+ codexArgs,
526
+ codexCommand: normalizeText(codexCommand),
527
+ executionMode: codexAppServerExecutionMode({ executionMode }),
528
+ processCwd: normalizeText(processCwd),
529
+ runtimeDir: normalizeText(runtimeDir)
530
+ }));
531
+ }
532
+
533
+ const codexAppServerRuntimePreparations = new Map();
534
+
535
+ /** The production shared runtime: one process owner per stable runtime directory.
536
+ * Execution and credential facilities retain host policy; threads do not own this process.
537
+ */
538
+ export function createCodexAppServerRuntime({ execution, credentials, configuration = {} }) {
539
+ const {
540
+ clientInfo = { name: "jskit", title: "JSKIT", version: "0.1.0" },
541
+ commandHookCommand,
542
+ errorPrefix = "",
543
+ modelCatalogEnvironmentName = "JSKIT_CODEX_MODEL_CATALOG_SOURCE",
544
+ processLabel = "jskit-codex-app-server",
545
+ processPath = fileURLToPath(new URL("./codexProcessLeader.js", import.meta.url)),
546
+ runtimeTokenEnvironmentName = "JSKIT_EXECUTION_RUNTIME_TOKEN",
547
+ commandHashEnvironmentName = "JSKIT_EXECUTION_COMMAND_HASH",
548
+ socketPathHelp = "Configure a shorter host runtime directory.",
549
+ umask = "0007"
550
+ } = configuration;
551
+ const CODEX_APP_SERVER_PROCESS_PATH = processPath;
552
+ const CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV = runtimeTokenEnvironmentName;
553
+ const CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV = commandHashEnvironmentName;
554
+ const codexProcessIdentity = createConversationProcessIdentity({
555
+ runtimeTokenEnvironmentName, commandHashEnvironmentName,
556
+ label: "Codex app-server",
557
+ unavailableCode: `${errorPrefix}codex_app_server_process_identity_unavailable`
558
+ });
559
+ const codexAppServerProcessIdentityIsWellFormed = codexProcessIdentity.isValid;
560
+ const captureCodexAppServerProcessIdentity = codexProcessIdentity.capture;
561
+ const currentCodexAccountIdentitySignature = credentials.identity;
562
+ const currentCodexAuthStateSignature = credentials.generation;
563
+ const assertCodexAuthGenerationCurrent = credentials.assertCurrent;
564
+
565
+ function codexAppServerSessionCommandHookConfig() {
566
+ const command = commandHookCommand;
567
+ return `hooks.PreToolUse=[{matcher="^Bash$",hooks=[{type="command",command=${JSON.stringify(command)},timeout=30}]}]`;
568
+ }
569
+
570
+ async function acquireRuntimeLock(runtimeDir = "", options = {}) {
571
+ await execution.prepare?.();
572
+ return acquireCodexRuntimeLock(runtimeDir, { ...options, busyCode: `${errorPrefix}codex_app_server_runtime_busy` });
573
+ }
574
+
575
+ const CODEX_APP_SERVER_MANAGED_UMASK = umask;
576
+
577
+ const CODEX_APP_SERVER_MANAGED_SHELL = "/bin/sh";
578
+
579
+ const CODEX_APP_SERVER_MANAGED_STARTUP_SCRIPT = [
580
+ `umask ${CODEX_APP_SERVER_MANAGED_UMASK}`,
581
+ `unset ${[...CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES].join(" ")}`,
582
+ 'exec "$@"'
583
+ ].join("\n");
584
+
585
+ const CODEX_APP_SERVER_HELPER_STARTUP_SCRIPT = [
586
+ `umask ${CODEX_APP_SERVER_MANAGED_UMASK}`,
587
+ "exec /usr/bin/env -i \\",
588
+ ' HOME="$HOME" LOGNAME="$LOGNAME" USER="$USER" PATH="$PATH" \\',
589
+ ' CODEX_HOME="$CODEX_HOME" LANG="$LANG" LC_ALL="$LC_ALL" LC_CTYPE="$LC_CTYPE" \\',
590
+ ` ${CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV}="$${CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV}" ${CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV}="$${CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV}" \\`,
591
+ ' SSL_CERT_DIR="$SSL_CERT_DIR" SSL_CERT_FILE="$SSL_CERT_FILE" TERM="$TERM" TMPDIR="$TMPDIR" TZ="$TZ" \\',
592
+ ' "$@"'
593
+ ].join("\n");
594
+
595
+ function codexAppServerProcessMetadataIsIdentifiable(metadata = {}, runtimeDir = "") {
596
+ const normalizedRuntimeDir = normalizeText(runtimeDir);
597
+ return Boolean(
598
+ Number(metadata.schemaVersion) === CODEX_APP_SERVER_METADATA_SCHEMA_VERSION &&
599
+ metadata.provider === CODEX_APP_SERVER_PROVIDER_ID &&
600
+ Number.isSafeInteger(Number(metadata.pid)) && Number(metadata.pid) > 0 &&
601
+ (!normalizedRuntimeDir || metadata.runtimeDir === normalizedRuntimeDir) &&
602
+ codexAppServerProcessIdentityIsWellFormed(metadata.processIdentity) &&
603
+ [
604
+ CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
605
+ CODEX_APP_SERVER_PROCESS_STATE.STOPPED
606
+ ].includes(metadata.processState)
607
+ );
608
+ }
609
+
610
+ async function inspectCodexAppServerProcessIdentity(metadata = {}, options = {}) {
611
+ if (typeof options.processIdentityInspector === "function") {
612
+ return codexProcessIdentity.normalizeInspection(await options.processIdentityInspector(metadata));
613
+ }
614
+ if (!codexAppServerProcessMetadataIsIdentifiable(metadata)) {
615
+ return { processGroupIds: [], status: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.INVALID };
616
+ }
617
+ return codexProcessIdentity.inspect(metadata);
618
+ }
619
+
620
+ async function stopCodexAppServerProcessGroup(metadata = {}, options = {}) {
621
+ if (!codexAppServerProcessMetadataIsIdentifiable(metadata)) {
622
+ return {
623
+ identityStatus: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.INVALID,
624
+ processExitVerified: false,
625
+ stopped: false
626
+ };
627
+ }
628
+ return codexProcessIdentity.stop(metadata, {
629
+ ...options,
630
+ processIdentityInspector: (current) => inspectCodexAppServerProcessIdentity(current, options)
631
+ });
632
+ }
633
+
634
+ function assertCodexAppServerSocketPathSupported(socketPath = "") {
635
+ if (!codexAppServerSocketPathTooLong(socketPath)) {
636
+ return;
637
+ }
638
+ throw new Error(
639
+ `Codex app-server Unix socket path is too long for this OS: ${socketPath} ` +
640
+ `(${codexAppServerSocketPathBytes(socketPath)} bytes, max ${CODEX_APP_SERVER_UNIX_SOCKET_PATH_MAX_BYTES}). ` +
641
+ socketPathHelp
642
+ );
643
+ }
644
+
645
+ async function stopOwnedCodexAppServerExecution(metadata = {}, options = {}) {
646
+ if (
647
+ metadata.processState === CODEX_APP_SERVER_PROCESS_STATE.STOPPED &&
648
+ metadata.processExitVerifiedAt
649
+ ) {
650
+ return stopCodexAppServerProcessGroup(metadata, options);
651
+ }
652
+ const executionId = normalizeText(metadata.executionId);
653
+ if (executionId) {
654
+ const stopExecution = typeof options.stopExecution === "function"
655
+ ? options.stopExecution
656
+ : execution.stop;
657
+ let executionStop;
658
+ try {
659
+ executionStop = await stopExecution(executionId, {
660
+ allowMissingRecordScopeRecovery: true,
661
+ killTimeoutMs: options.killTimeoutMs,
662
+ reason: options.reason || "codex-app-server-stop",
663
+ termTimeoutMs: options.termTimeoutMs
664
+ });
665
+ } catch (error) {
666
+ return {
667
+ error: String(error?.message || error || "The owned execution could not be stopped."),
668
+ executionId,
669
+ processExitVerified: false,
670
+ scopeEmpty: false,
671
+ stopped: false
672
+ };
673
+ }
674
+ if (executionStop?.ok === true && executionStop?.scopeEmpty === true) {
675
+ return {
676
+ ...executionStop,
677
+ executionId,
678
+ processExitVerified: true,
679
+ stopped: executionStop.stopped === true
680
+ };
681
+ }
682
+ if (typeof options.stopExecution === "function" || execution.authoritative) {
683
+ return {
684
+ ...(executionStop && typeof executionStop === "object" ? executionStop : {}),
685
+ executionId,
686
+ processExitVerified: false,
687
+ scopeEmpty: false,
688
+ stopped: false
689
+ };
690
+ }
691
+ }
692
+ return stopCodexAppServerProcessGroup(metadata, options);
693
+ }
694
+
695
+ async function stopCodexAppServerProcess(runtimeDir = "", options = {}) {
696
+ const normalizedRuntimeDir = normalizeText(runtimeDir);
697
+ if (!normalizedRuntimeDir) {
698
+ return {
699
+ processExitVerified: false,
700
+ stopped: false
701
+ };
702
+ }
703
+ const metadata = await readCodexAppServerMetadata(normalizedRuntimeDir);
704
+ if (!codexAppServerProcessMetadataIsIdentifiable(metadata, normalizedRuntimeDir)) {
705
+ return {
706
+ identityStatus: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.INVALID,
707
+ processExitVerified: false,
708
+ stopped: false
709
+ };
710
+ }
711
+ return stopOwnedCodexAppServerExecution(metadata, options);
712
+ }
713
+
714
+ async function cleanupFailedCodexAppServerStart(runtimeDir = "", {
715
+ helper = false,
716
+ executionId = "",
717
+ neverStarted = false,
718
+ pid = null,
719
+ processIdentity = null
720
+ } = {}, options = {}) {
721
+ let processStop = {
722
+ processExitVerified: neverStarted === true,
723
+ scopeEmpty: neverStarted === true,
724
+ stopped: false
725
+ };
726
+ let processCleanupFailed = false;
727
+ if (!neverStarted) {
728
+ try {
729
+ processStop = await stopOwnedCodexAppServerExecution({
730
+ executionId: normalizeText(executionId),
731
+ pid: Number(pid),
732
+ processIdentity,
733
+ processState: CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
734
+ provider: CODEX_APP_SERVER_PROVIDER_ID,
735
+ runtimeDir: normalizeText(runtimeDir),
736
+ schemaVersion: CODEX_APP_SERVER_METADATA_SCHEMA_VERSION
737
+ }, options);
738
+ } catch {
739
+ processCleanupFailed = true;
740
+ }
741
+ }
742
+ if (processCleanupFailed || processStop.processExitVerified !== true) {
743
+ return {
744
+ ...processStop,
745
+ cleanupFailed: true
746
+ };
747
+ }
748
+ const helperRemovals = helper
749
+ ? [
750
+ rm(codexAppServerHelperHomeDir(runtimeDir), { force: true, recursive: true }),
751
+ rm(codexAppServerHelperWorkspaceDir(runtimeDir), { force: true, recursive: true }),
752
+ rm(codexAppServerLogPath(runtimeDir), { force: true })
753
+ ]
754
+ : [];
755
+ const removals = await Promise.allSettled([
756
+ ...helperRemovals,
757
+ rm(codexAppServerSocketPath(runtimeDir), { force: true }),
758
+ rm(codexAppServerMetadataPath(runtimeDir), { force: true }),
759
+ removeCodexAppServerMetadataTemps(runtimeDir)
760
+ ]);
761
+ return {
762
+ ...processStop,
763
+ cleanupFailed: removals.some(({ status }) => status === "rejected")
764
+ };
765
+ }
766
+
767
+ async function stopCodexAppServerRuntime(options = {}) {
768
+ const runtimeDir = normalizeText(options.runtimeDir);
769
+ const preserveProcessExitProof = options.preserveProcessExitProof === true;
770
+ if (!runtimeDir || !codexAppServerRuntimeDirIsManaged(runtimeDir)) {
771
+ return {
772
+ processExitVerified: false,
773
+ runtimeDirPreserved: false,
774
+ runtimeDirRemoved: false,
775
+ stopped: false
776
+ };
777
+ }
778
+ let releaseLock;
779
+ try {
780
+ releaseLock = await acquireRuntimeLock(runtimeDir, options);
781
+ } catch (error) {
782
+ if (!codexAppServerRuntimeCleanupCanSkip(error)) {
783
+ throw error;
784
+ }
785
+ return {
786
+ processExitVerified: false,
787
+ runtimeDirCleanupError: String(error?.message || error || ""),
788
+ runtimeDirCleanupSkipped: String(error?.code || "") !== "ENOENT",
789
+ runtimeDirPreserved: false,
790
+ runtimeDirRemoved: false,
791
+ stopped: false
792
+ };
793
+ }
794
+ let runtimeDirRemoved = false;
795
+ let runtimeDirCleanupSkipped = false;
796
+ let runtimeDirCleanupError = "";
797
+ let runtimeDirPreserved = false;
798
+ let processStop = {
799
+ processExitVerified: false,
800
+ stopped: false
801
+ };
802
+ try {
803
+ const existing = await readCodexAppServerMetadata(runtimeDir);
804
+ const expectedAccount = normalizeText(options.expectedAccountIdentitySignature);
805
+ const ownedRuntime = options.ownedRuntime;
806
+ if (
807
+ ownedRuntime &&
808
+ codexAppServerProcessMetadataIsIdentifiable(ownedRuntime, runtimeDir) &&
809
+ (!expectedAccount || ownedRuntime.accountIdentitySignature === expectedAccount) &&
810
+ (!existing || codexAppServerRuntimeIdentity(existing) !== codexAppServerRuntimeIdentity(ownedRuntime))
811
+ ) {
812
+ // Another owner may already have removed or replaced the shared runtime.
813
+ // Prove this provider's exact execution stopped without touching its successor.
814
+ return {
815
+ ...await stopOwnedCodexAppServerExecution(ownedRuntime, options),
816
+ runtimeDirPreserved: Boolean(existing),
817
+ runtimeDirRemoved: false
818
+ };
819
+ }
820
+ if (!existing) {
821
+ // Changeover after a server restart may have no runtime.json left. The
822
+ // managed execution owner can still drain and prove its exact scope empty.
823
+ // A missing file alone is never process-exit proof, and account-specific
824
+ // cleanup must keep using its retained execution identity above.
825
+ const owned = options.verifyOwnerScope === true && !expectedAccount
826
+ ? await execution.stopOwned({
827
+ kind: "assistant",
828
+ operationId: "codex-app-server",
829
+ ownerId: normalizeText(options.ownerId) || stableHash(runtimeDir)
830
+ }, { reason: "codex-app-server-changeover" })
831
+ : null;
832
+ const ownerExitVerified = owned?.supported === true && owned?.ok === true && owned?.scopeEmpty === true;
833
+ return {
834
+ processExitVerified: ownerExitVerified,
835
+ runtimeDirPreserved: false,
836
+ runtimeDirRemoved: false,
837
+ stopped: ownerExitVerified && Number(owned?.closed) > 0
838
+ };
839
+ }
840
+ if (expectedAccount) {
841
+ // A replacement runtime is installed only after the previous process has
842
+ // drained under this same lock. Never stop that replacement for stale
843
+ // ephemeral-thread cleanup.
844
+ if (!codexAppServerProcessMetadataIsIdentifiable(existing, runtimeDir) ||
845
+ !/^sha256:[a-f0-9]{64}$/u.test(existing.accountIdentitySignature)) {
846
+ return { processExitVerified: false, runtimeDirRemoved: false, stopped: false };
847
+ }
848
+ if (existing.accountIdentitySignature !== expectedAccount) {
849
+ return { ownershipSuperseded: true, processExitVerified: false, runtimeDirRemoved: false, stopped: false };
850
+ }
851
+ }
852
+ processStop = await stopCodexAppServerProcess(runtimeDir, options);
853
+ if (processStop.processExitVerified === true && preserveProcessExitProof) {
854
+ const metadata = await readCodexAppServerMetadata(runtimeDir);
855
+ if (!codexAppServerProcessMetadataIsIdentifiable(metadata, runtimeDir)) {
856
+ return {
857
+ ...processStop,
858
+ processExitVerified: false,
859
+ runtimeDirPreserved: false,
860
+ runtimeDirRemoved: false
861
+ };
862
+ }
863
+ await writeCodexAppServerMetadata(runtimeDir, {
864
+ ...metadata,
865
+ processExitVerifiedAt: metadata.processExitVerifiedAt || new Date().toISOString(),
866
+ processState: CODEX_APP_SERVER_PROCESS_STATE.STOPPED
867
+ });
868
+ runtimeDirPreserved = true;
869
+ } else if (processStop.processExitVerified === true) {
870
+ try {
871
+ runtimeDirRemoved = await removeCodexAppServerRuntimeDir(runtimeDir);
872
+ } catch (error) {
873
+ if (!codexAppServerRuntimeCleanupCanSkip(error)) {
874
+ throw error;
875
+ }
876
+ runtimeDirCleanupSkipped = true;
877
+ runtimeDirCleanupError = String(error?.message || error || "");
878
+ }
879
+ }
880
+ } finally {
881
+ await releaseLock();
882
+ }
883
+ return {
884
+ ...processStop,
885
+ runtimeDirCleanupError,
886
+ runtimeDirCleanupSkipped,
887
+ runtimeDirPreserved,
888
+ runtimeDirRemoved
889
+ };
890
+ }
891
+
892
+ function codexAppServerMetadataIsWellFormed(metadata = {}, options = {}) {
893
+ const expectedAttachmentHostRoot = options.compatibility.attachmentHostRoot;
894
+ const expectedAccountIdentitySignature = normalizeText(options.accountIdentitySignature);
895
+ const expectedExecutionMode = codexAppServerExecutionMode(options);
896
+ const expectedToolHomeSource = codexAppServerIsHelper(options) ? "" : normalizeText(options.toolHomeSource);
897
+ const expectedTerminalEnvHash = codexAppServerTerminalEnvHash(codexAppServerIsHelper(options) ? {} : options.terminalEnv);
898
+ const expectedRuntimesHash = options.compatibility.runtimesHash;
899
+ const expectedExecutionContextHash = options.compatibility.executionContextHash;
900
+ const expectedHelperProcessCwd = expectedExecutionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
901
+ ? codexAppServerHelperWorkspaceDir(metadata.runtimeDir)
902
+ : "";
903
+ return Boolean(
904
+ metadata.schemaVersion === CODEX_APP_SERVER_METADATA_SCHEMA_VERSION &&
905
+ metadata.accountIdentitySignature === expectedAccountIdentitySignature &&
906
+ metadata.attachmentHostRoot === expectedAttachmentHostRoot &&
907
+ metadata.authStateSignature &&
908
+ metadata.executionId &&
909
+ metadata.executionContextHash === expectedExecutionContextHash &&
910
+ metadata.executionMode === expectedExecutionMode &&
911
+ metadata.processCwd &&
912
+ codexAppServerProcessMetadataIsIdentifiable(metadata) &&
913
+ metadata.processState === CODEX_APP_SERVER_PROCESS_STATE.RUNNING &&
914
+ (expectedExecutionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER ||
915
+ codexHistoryAdapterUrlIsValid(metadata.historyAdapterBaseUrl, metadata.processIdentity.runtimeToken)) &&
916
+ (!expectedHelperProcessCwd || metadata.processCwd === expectedHelperProcessCwd) &&
917
+ metadata.provider === CODEX_APP_SERVER_PROVIDER_ID &&
918
+ metadata.runtimesHash === expectedRuntimesHash &&
919
+ metadata.terminalEnvHash === expectedTerminalEnvHash &&
920
+ metadata.toolHomeSource === expectedToolHomeSource &&
921
+ metadata.transport === CODEX_APP_SERVER_TRANSPORT.UNIX &&
922
+ metadata.endpoint &&
923
+ metadata.socketPath
924
+ );
925
+ }
926
+
927
+ async function codexAppServerEndpointStatus(endpoint = "", {
928
+ timeoutMs = CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS,
929
+ WebSocketImpl = WebSocket
930
+ } = {}) {
931
+ const normalizedEndpoint = normalizeText(endpoint);
932
+ const socketPath = socketPathFromCodexAppServerEndpoint(normalizedEndpoint);
933
+ if (!socketPath || !await fileExists(socketPath)) {
934
+ return {
935
+ status: CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING
936
+ };
937
+ }
938
+ const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS);
939
+ const client = new CodexAppServerJsonRpcClient({
940
+ endpoint: normalizedEndpoint,
941
+ requestTimeoutMs: normalizedTimeoutMs,
942
+ WebSocketImpl
943
+ });
944
+ let timeout = null;
945
+ const probe = (async () => {
946
+ await client.connect();
947
+ await client.initialize({ clientInfo: clientInfo });
948
+ return CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE;
949
+ })();
950
+ probe.catch(() => null);
951
+ try {
952
+ const status = await Promise.race([
953
+ probe,
954
+ new Promise((resolve) => {
955
+ timeout = setTimeout(() => resolve(CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT), normalizedTimeoutMs);
956
+ timeout.unref?.();
957
+ })
958
+ ]);
959
+ return {
960
+ status
961
+ };
962
+ } catch {
963
+ return {
964
+ status: CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE
965
+ };
966
+ } finally {
967
+ clearTimeout(timeout);
968
+ client.close();
969
+ }
970
+ }
971
+
972
+ async function codexAppServerMetadataIsLive(metadata = {}, options = {}) {
973
+ return (await codexAppServerRuntimeStatus(metadata, options)).status === CODEX_APP_SERVER_RUNTIME_STATUS.LIVE;
974
+ }
975
+
976
+ async function codexAppServerRuntimeStatus(metadata = {}, options = {}) {
977
+ const normalized = normalizeCodexAppServerMetadata(metadata);
978
+ if (
979
+ codexAppServerProcessMetadataIsIdentifiable(normalized) &&
980
+ normalized.processState === CODEX_APP_SERVER_PROCESS_STATE.STOPPED &&
981
+ normalized.processExitVerifiedAt
982
+ ) {
983
+ return {
984
+ metadata: normalized,
985
+ replace: true,
986
+ reusable: false,
987
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.EXITED
988
+ };
989
+ }
990
+ if (!codexAppServerMetadataIsWellFormed(normalized, options)) {
991
+ return {
992
+ metadata: normalized,
993
+ replace: true,
994
+ reusable: false,
995
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.INCOMPATIBLE
996
+ };
997
+ }
998
+ const authStateSignature = await currentCodexAuthStateSignature(options);
999
+ if (normalized.authStateSignature !== authStateSignature) {
1000
+ return {
1001
+ metadata: normalized,
1002
+ replace: true,
1003
+ reusable: false,
1004
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.INCOMPATIBLE
1005
+ };
1006
+ }
1007
+ let processIdentity = null;
1008
+ if (typeof options.processGroupIsAlive === "function") {
1009
+ const alive = options.processGroupIsAlive(normalized.pid);
1010
+ processIdentity = {
1011
+ processGroupIds: alive ? [normalized.pid] : [],
1012
+ status: alive
1013
+ ? CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.EXACT
1014
+ : CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.ABSENT
1015
+ };
1016
+ } else {
1017
+ processIdentity = await inspectCodexAppServerProcessIdentity(normalized, options);
1018
+ }
1019
+ if (
1020
+ processIdentity.status === CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.ABSENT ||
1021
+ processIdentity.status === CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.MISMATCH
1022
+ ) {
1023
+ return {
1024
+ metadata: normalized,
1025
+ replace: true,
1026
+ reusable: false,
1027
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.EXITED
1028
+ };
1029
+ }
1030
+ if (processIdentity.status !== CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.EXACT) {
1031
+ return {
1032
+ metadata: normalized,
1033
+ replace: false,
1034
+ reusable: true,
1035
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.SUSPECT
1036
+ };
1037
+ }
1038
+ const endpoint = await codexAppServerEndpointStatus(normalized.endpoint, {
1039
+ timeoutMs: codexAppServerLivenessTimeoutMs(options),
1040
+ WebSocketImpl: options.WebSocketImpl
1041
+ });
1042
+ if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE) {
1043
+ return {
1044
+ metadata: normalized,
1045
+ replace: false,
1046
+ reusable: true,
1047
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.LIVE
1048
+ };
1049
+ }
1050
+ if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING) {
1051
+ return {
1052
+ metadata: normalized,
1053
+ replace: true,
1054
+ reusable: false,
1055
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.MISSING
1056
+ };
1057
+ }
1058
+ if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT) {
1059
+ return {
1060
+ metadata: normalized,
1061
+ replace: false,
1062
+ reusable: true,
1063
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.SUSPECT
1064
+ };
1065
+ }
1066
+ return {
1067
+ metadata: normalized,
1068
+ replace: true,
1069
+ reusable: false,
1070
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.MISSING
1071
+ };
1072
+ }
1073
+
1074
+ async function waitForCodexAppServer(endpoint = "", {
1075
+ timeoutMs = CODEX_APP_SERVER_READY_TIMEOUT_MS,
1076
+ WebSocketImpl = WebSocket
1077
+ } = {}) {
1078
+ const socketPath = socketPathFromCodexAppServerEndpoint(endpoint);
1079
+ if (!socketPath) {
1080
+ return false;
1081
+ }
1082
+ const startedAt = Date.now();
1083
+ while (!await fileExists(socketPath) && Date.now() - startedAt <= timeoutMs) {
1084
+ await delay(100);
1085
+ }
1086
+ const remainingMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
1087
+ if (!await fileExists(socketPath) || remainingMs <= 0) {
1088
+ return false;
1089
+ }
1090
+ const client = new CodexAppServerJsonRpcClient({
1091
+ endpoint,
1092
+ requestTimeoutMs: remainingMs,
1093
+ WebSocketImpl
1094
+ });
1095
+ let timeout = null;
1096
+ const handshake = (async () => {
1097
+ await client.connect();
1098
+ await client.initialize({ clientInfo: clientInfo });
1099
+ return true;
1100
+ })();
1101
+ handshake.catch(() => null);
1102
+ try {
1103
+ return await Promise.race([
1104
+ handshake,
1105
+ new Promise((resolve) => {
1106
+ timeout = setTimeout(() => resolve(false), remainingMs);
1107
+ timeout.unref?.();
1108
+ })
1109
+ ]);
1110
+ } catch {
1111
+ return false;
1112
+ } finally {
1113
+ clearTimeout(timeout);
1114
+ client.close();
1115
+ }
1116
+ }
1117
+
1118
+ async function startCodexAppServerProcess({
1119
+ providerHome = false,
1120
+ accountIdentitySignature = "",
1121
+ authStateSignature = "",
1122
+ codexCommand = "codex",
1123
+ env = process.env,
1124
+ executionRoot = "",
1125
+ executionMode = CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
1126
+ killTimeoutMs = 0,
1127
+ processIdentityInspector = null,
1128
+ readyTimeoutMs = CODEX_APP_SERVER_READY_TIMEOUT_MS,
1129
+ signalProcessGroup: signalProcessGroupOverride = null,
1130
+ stopExecution = null,
1131
+ terminalEnv = {},
1132
+ termTimeoutMs = 0,
1133
+ toolHomeSource = "",
1134
+ WebSocketImpl = WebSocket,
1135
+ workdir = "",
1136
+ socketOwnerDrained = false,
1137
+ runtimeDir,
1138
+ ownerId = "",
1139
+ compatibility
1140
+ } = {}) {
1141
+ const processLifecycleOptions = {
1142
+ killTimeoutMs,
1143
+ processIdentityInspector,
1144
+ signalProcessGroup: signalProcessGroupOverride,
1145
+ stopExecution,
1146
+ termTimeoutMs
1147
+ };
1148
+ await ensureWritablePrivateDirectory(runtimeDir);
1149
+ const helper = codexAppServerExecutionMode({ executionMode }) ===
1150
+ CODEX_APP_SERVER_EXECUTION_MODES.HELPER;
1151
+ const normalizedToolHomeSource = normalizeText(toolHomeSource);
1152
+ if (normalizedToolHomeSource) {
1153
+ await assertExistingDirectory(normalizedToolHomeSource, "Codex credential home");
1154
+ }
1155
+ const resolvedAuthStateSignature = await currentCodexAuthStateSignature({ authStateSignature });
1156
+ await assertCodexAuthGenerationCurrent(resolvedAuthStateSignature);
1157
+ const socketPath = codexAppServerSocketPath(runtimeDir);
1158
+ assertCodexAppServerSocketPathSupported(socketPath);
1159
+ const endpoint = codexAppServerUnixEndpoint(socketPath);
1160
+ const logPath = codexAppServerLogPath(runtimeDir);
1161
+ const helperHome = helper ? codexAppServerHelperHomeDir(runtimeDir) : "";
1162
+ const helperWorkspace = helper ? codexAppServerHelperWorkspaceDir(runtimeDir) : "";
1163
+ if (helper) {
1164
+ await Promise.all([
1165
+ rm(helperHome, { force: true, recursive: true }),
1166
+ rm(helperWorkspace, { force: true, recursive: true })
1167
+ ]);
1168
+ await Promise.all([
1169
+ ensureWritablePrivateDirectory(helperHome),
1170
+ ensureWritablePrivateDirectory(helperWorkspace)
1171
+ ]);
1172
+ if (providerHome) {
1173
+ // The canonical home contains only our curated provider projection. Copy
1174
+ // it into the existing isolated helper home; never load OpenAI auth here.
1175
+ const sourceHome = path.join(normalizedToolHomeSource, ".codex");
1176
+ const catalogPath = path.join(helperHome, "models.json");
1177
+ const config = await readFile(path.join(sourceHome, "config.toml"), "utf8");
1178
+ await writeFile(catalogPath, await readFile(path.join(sourceHome, "models.json")), { mode: 0o600 });
1179
+ await writeFile(path.join(helperHome, "config.toml"), config.replace(
1180
+ JSON.stringify(path.join(sourceHome, "models.json")), JSON.stringify(catalogPath)
1181
+ ), { mode: 0o600 });
1182
+ }
1183
+ }
1184
+ const processCwd = helper
1185
+ ? helperWorkspace
1186
+ : codexAppServerProcessCwd({
1187
+ executionRoot,
1188
+ runtimeDir,
1189
+ workdir
1190
+ });
1191
+ const projectTrustOverride = helper ? "" : codexAppServerProjectTrustOverride(workdir);
1192
+ const normalizedTerminalEnv = normalizeCodexAppServerTerminalEnv(helper ? {} : terminalEnv);
1193
+ const commandBaseEnv = helper
1194
+ ? codexAppServerHelperCommandBaseEnv(env, helperHome)
1195
+ : codexAppServerCommandBaseEnv({
1196
+ env,
1197
+ terminalEnv: normalizedTerminalEnv
1198
+ });
1199
+ if (await fileExists(socketPath) && !socketOwnerDrained) {
1200
+ const endpointStatus = await codexAppServerEndpointStatus(endpoint, {
1201
+ timeoutMs: CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS,
1202
+ WebSocketImpl
1203
+ });
1204
+ if (![CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING, CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE]
1205
+ .includes(endpointStatus.status)) {
1206
+ const error = new Error("The existing Codex app-server socket still has an unretired owner.");
1207
+ error.code = `${errorPrefix}codex_app_server_socket_owner_unverified`;
1208
+ error.retryable = false;
1209
+ throw error;
1210
+ }
1211
+ }
1212
+ await rm(socketPath, {
1213
+ force: true
1214
+ });
1215
+ const codexArgs = [
1216
+ // Keep every override after app-server: a trailing -c (including the model
1217
+ // catalogue) otherwise drops the earlier ones. See openai/codex#39012.
1218
+ "app-server",
1219
+ "--listen",
1220
+ endpoint,
1221
+ // App-server uses config overrides, not the top-level sandbox bypass flag.
1222
+ ...(helper ? [] : [
1223
+ "-c",
1224
+ 'approval_policy="never"',
1225
+ "-c",
1226
+ 'sandbox_mode="danger-full-access"',
1227
+ "-c",
1228
+ `features.hooks=${Boolean(commandHookCommand)}`,
1229
+ ...(commandHookCommand ? ["-c", codexAppServerSessionCommandHookConfig()] : [])
1230
+ ]),
1231
+ "-c",
1232
+ "check_for_update_on_startup=false",
1233
+ ...(projectTrustOverride
1234
+ ? [
1235
+ "-c",
1236
+ projectTrustOverride
1237
+ ]
1238
+ : [])
1239
+ ];
1240
+ const runtimeToken = randomUUID();
1241
+ const commandHash = codexAppServerProcessCommandHash({
1242
+ codexArgs,
1243
+ codexCommand,
1244
+ executionMode,
1245
+ processCwd,
1246
+ runtimeDir
1247
+ });
1248
+ const baseEnv = {
1249
+ ...commandBaseEnv,
1250
+ // A curated provider home contains a provider-only static catalogue. Use
1251
+ // the binary's native definitions as the base for its shared runtime.
1252
+ [modelCatalogEnvironmentName]: providerHome ? "bundled" : "native",
1253
+ [CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV]: commandHash,
1254
+ [CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV]: runtimeToken
1255
+ };
1256
+ const profileBinary = path.isAbsolute(codexCommand) ? await stat(codexCommand).catch(() => null) : null;
1257
+ const startResult = await execution.run({
1258
+ args: [
1259
+ "-c",
1260
+ helper ? CODEX_APP_SERVER_HELPER_STARTUP_SCRIPT : CODEX_APP_SERVER_MANAGED_STARTUP_SCRIPT,
1261
+ processLabel,
1262
+ ...(helper ? [] : [process.execPath, CODEX_APP_SERVER_PROCESS_PATH, runtimeDir]),
1263
+ codexCommand,
1264
+ ...codexArgs
1265
+ ],
1266
+ baseEnv,
1267
+ command: CODEX_APP_SERVER_MANAGED_SHELL,
1268
+ credentialHome: codexAppServerCredentialHome(
1269
+ helper ? helperHome : normalizedToolHomeSource,
1270
+ baseEnv
1271
+ ),
1272
+ cwd: processCwd || process.cwd(),
1273
+ execution: {
1274
+ kind: "assistant",
1275
+ label: "Codex assistant",
1276
+ lifecycle: "service",
1277
+ operationId: "codex-app-server",
1278
+ ownerId: normalizeText(ownerId) || stableHash(runtimeDir)
1279
+ },
1280
+ processProfile: profileBinary?.isFile() ? {
1281
+ command: codexCommand, helper, executionMode,
1282
+ binary: [profileBinary.dev, profileBinary.ino, profileBinary.size, profileBinary.mtimeMs, profileBinary.ctimeMs],
1283
+ platform: process.platform, architecture: process.arch,
1284
+ startup: helper ? CODEX_APP_SERVER_HELPER_STARTUP_SCRIPT : CODEX_APP_SERVER_MANAGED_STARTUP_SCRIPT,
1285
+ historyAdapter: !helper
1286
+ } : null,
1287
+ logPath,
1288
+ inheritProcessEnv: false,
1289
+ mode: "detached",
1290
+ timeout: readyTimeoutMs
1291
+ });
1292
+ if (!startResult.ok) {
1293
+ const failedProcessIdentity = await captureCodexAppServerProcessIdentity({
1294
+ commandHash,
1295
+ pid: startResult.pid,
1296
+ reportedIdentity: startResult.processIdentity,
1297
+ runtimeToken
1298
+ }).catch(() => null);
1299
+ const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
1300
+ helper,
1301
+ executionId: startResult.execution?.id,
1302
+ neverStarted: !Number.isSafeInteger(Number(startResult.pid)),
1303
+ pid: startResult.pid,
1304
+ processIdentity: failedProcessIdentity
1305
+ }, processLifecycleOptions);
1306
+ const error = new Error(
1307
+ startResult.output || startResult.error ||
1308
+ (helper ? "Codex isolated helper runtime failed to start." : "Codex app-server failed to start.")
1309
+ );
1310
+ error.code = cleanup.cleanupFailed
1311
+ ? (helper
1312
+ ? `${errorPrefix}codex_helper_runtime_cleanup_required`
1313
+ : `${errorPrefix}codex_app_server_cleanup_required`)
1314
+ : (startResult.code || (helper
1315
+ ? `${errorPrefix}codex_helper_runtime_start_failed`
1316
+ : `${errorPrefix}codex_app_server_start_failed`));
1317
+ error.cleanupRequired = cleanup.cleanupFailed;
1318
+ error.execution = startResult.execution;
1319
+ error.retryable = cleanup.cleanupFailed ? false : startResult.retryable === true;
1320
+ throw error;
1321
+ }
1322
+
1323
+ let processIdentity;
1324
+ try {
1325
+ processIdentity = await captureCodexAppServerProcessIdentity({
1326
+ commandHash,
1327
+ pid: startResult.pid,
1328
+ reportedIdentity: startResult.processIdentity,
1329
+ runtimeToken
1330
+ });
1331
+ } catch (cause) {
1332
+ const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
1333
+ helper,
1334
+ executionId: startResult.execution?.id,
1335
+ pid: startResult.pid,
1336
+ processIdentity: startResult.processIdentity
1337
+ }, processLifecycleOptions);
1338
+ const error = new Error("Codex app-server process ownership could not be recorded.", {
1339
+ cause
1340
+ });
1341
+ error.code = cleanup.cleanupFailed
1342
+ ? `${errorPrefix}codex_app_server_cleanup_required`
1343
+ : `${errorPrefix}codex_app_server_process_identity_unavailable`;
1344
+ error.cleanupRequired = cleanup.cleanupFailed;
1345
+ throw error;
1346
+ }
1347
+
1348
+ const ready = await waitForCodexAppServer(endpoint, {
1349
+ timeoutMs: readyTimeoutMs,
1350
+ WebSocketImpl
1351
+ });
1352
+ if (!ready) {
1353
+ const logTail = await tailTextFile(logPath);
1354
+ const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
1355
+ helper,
1356
+ executionId: startResult.execution?.id,
1357
+ pid: startResult.pid,
1358
+ processIdentity
1359
+ }, processLifecycleOptions);
1360
+ if (!cleanup.cleanupFailed && !helper) {
1361
+ await credentials.observeFailure(logTail);
1362
+ }
1363
+ const error = new Error([
1364
+ helper
1365
+ ? "Codex isolated helper runtime did not become ready."
1366
+ : `Codex app-server did not become ready at ${endpoint}.`,
1367
+ logTail ? `Recent log output:\n${logTail}` : ""
1368
+ ].filter(Boolean).join("\n"));
1369
+ error.code = cleanup.cleanupFailed
1370
+ ? (helper
1371
+ ? `${errorPrefix}codex_helper_runtime_cleanup_required`
1372
+ : `${errorPrefix}codex_app_server_cleanup_required`)
1373
+ : (helper
1374
+ ? `${errorPrefix}codex_helper_runtime_start_failed`
1375
+ : `${errorPrefix}codex_app_server_ready_timeout`);
1376
+ error.cleanupRequired = cleanup.cleanupFailed;
1377
+ error.retryable = false;
1378
+ throw error;
1379
+ }
1380
+
1381
+ let historyAdapterBaseUrl = "";
1382
+ try {
1383
+ if (!helper) {
1384
+ const descriptor = JSON.parse(await readFile(path.join(runtimeDir, "history-adapter.json"), "utf8"));
1385
+ if (descriptor.runtimeToken !== runtimeToken || !codexHistoryAdapterUrlIsValid(descriptor.baseUrl, runtimeToken)) {
1386
+ throw new Error("Codex history adapter did not confirm the current runtime.");
1387
+ }
1388
+ historyAdapterBaseUrl = descriptor.baseUrl;
1389
+ }
1390
+ await assertCodexAuthGenerationCurrent(resolvedAuthStateSignature);
1391
+ } catch (cause) {
1392
+ const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
1393
+ helper,
1394
+ executionId: startResult.execution?.id,
1395
+ pid: startResult.pid,
1396
+ processIdentity
1397
+ }, processLifecycleOptions);
1398
+ if (cleanup.cleanupFailed) {
1399
+ const error = new Error("Codex app-server authentication changed and its execution could not be drained.", {
1400
+ cause
1401
+ });
1402
+ error.code = `${errorPrefix}codex_app_server_cleanup_required`;
1403
+ error.cleanupRequired = true;
1404
+ error.retryable = false;
1405
+ throw error;
1406
+ }
1407
+ throw cause;
1408
+ }
1409
+
1410
+ return {
1411
+ accountIdentitySignature: normalizeText(accountIdentitySignature),
1412
+ attachmentHostRoot: compatibility.attachmentHostRoot,
1413
+ authStateSignature: resolvedAuthStateSignature,
1414
+ endpoint,
1415
+ executionId: normalizeText(startResult.execution?.id),
1416
+ executionMode: helper
1417
+ ? CODEX_APP_SERVER_EXECUTION_MODES.HELPER
1418
+ : CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
1419
+ executionContextHash: compatibility.executionContextHash,
1420
+ healthz: "",
1421
+ historyAdapterBaseUrl,
1422
+ logPath,
1423
+ pid: Number.isSafeInteger(Number(startResult.pid)) ? Number(startResult.pid) : null,
1424
+ processCwd,
1425
+ processIdentity,
1426
+ processState: CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
1427
+ provider: CODEX_APP_SERVER_PROVIDER_ID,
1428
+ readyz: "",
1429
+ runtimeDir,
1430
+ runtimesHash: compatibility.runtimesHash,
1431
+ schemaVersion: CODEX_APP_SERVER_METADATA_SCHEMA_VERSION,
1432
+ socketPath,
1433
+ startedAt: new Date().toISOString(),
1434
+ terminalEnvHash: codexAppServerTerminalEnvHash(normalizedTerminalEnv),
1435
+ toolHomeSource: helper ? "" : normalizedToolHomeSource,
1436
+ transport: CODEX_APP_SERVER_TRANSPORT.UNIX
1437
+ };
1438
+ }
1439
+
1440
+ async function ensureCodexAppServerRuntime(options = {}) {
1441
+ const runtimeDir = path.resolve(options.runtimeDir);
1442
+ while (codexAppServerRuntimePreparations.has(runtimeDir)) {
1443
+ await codexAppServerRuntimePreparations.get(runtimeDir);
1444
+ }
1445
+ // Each waiter rechecks its own auth and runtime configuration after startup.
1446
+ const operation = prepareCodexAppServerRuntime({ ...options, runtimeDir });
1447
+ codexAppServerRuntimePreparations.set(runtimeDir, operation);
1448
+ try {
1449
+ return await operation;
1450
+ } finally {
1451
+ codexAppServerRuntimePreparations.delete(runtimeDir);
1452
+ }
1453
+ }
1454
+
1455
+ async function prepareCodexAppServerRuntime(options) {
1456
+ const { runtimeDir } = options;
1457
+ const [accountIdentitySignature, authStateSignature] = await Promise.all([
1458
+ currentCodexAccountIdentitySignature(options),
1459
+ currentCodexAuthStateSignature(options)
1460
+ ]);
1461
+ const runtimeOptions = {
1462
+ ...options,
1463
+ accountIdentitySignature,
1464
+ authStateSignature
1465
+ };
1466
+ const runtimeMetadataWriter = typeof options.runtimeMetadataWriter === "function"
1467
+ ? options.runtimeMetadataWriter
1468
+ : writeCodexAppServerMetadata;
1469
+ await ensureWritablePrivateDirectory(runtimeDir);
1470
+
1471
+ const existing = await readCodexAppServerMetadata(runtimeDir);
1472
+ const existingStatus = existing ? await codexAppServerRuntimeStatus(existing, runtimeOptions) : null;
1473
+ if (existingStatus?.reusable) {
1474
+ return {
1475
+ ...existingStatus.metadata,
1476
+ reused: true,
1477
+ runtimeStatus: existingStatus.status
1478
+ };
1479
+ }
1480
+
1481
+ const releaseLock = await acquireRuntimeLock(runtimeDir, runtimeOptions);
1482
+ try {
1483
+ let socketOwnerDrained = false;
1484
+ const afterLock = await readCodexAppServerMetadata(runtimeDir);
1485
+ const afterLockStatus = afterLock ? await codexAppServerRuntimeStatus(afterLock, runtimeOptions) : null;
1486
+ if (afterLockStatus?.reusable) {
1487
+ return {
1488
+ ...afterLockStatus.metadata,
1489
+ reused: true,
1490
+ runtimeStatus: afterLockStatus.status
1491
+ };
1492
+ }
1493
+
1494
+ if (afterLock && (!afterLockStatus || afterLockStatus.replace !== false)) {
1495
+ const stopped = await stopCodexAppServerProcess(runtimeDir, runtimeOptions);
1496
+ if (stopped.processExitVerified !== true) {
1497
+ const error = new Error(
1498
+ `${clientInfo.title} found an earlier Codex app-server but could not prove that its owned execution was stopped. It refused to start a replacement because two assistant processes could damage the session or worsen resource pressure.`
1499
+ );
1500
+ error.code = `${errorPrefix}codex_app_server_process_identity_unverified`;
1501
+ error.cleanupRequired = true;
1502
+ error.retryable = false;
1503
+ throw error;
1504
+ }
1505
+ socketOwnerDrained = true;
1506
+ }
1507
+
1508
+ const started = await startCodexAppServerProcess({
1509
+ ...runtimeOptions,
1510
+ runtimeDir,
1511
+ socketOwnerDrained
1512
+ });
1513
+ try {
1514
+ await assertCodexAuthGenerationCurrent(started.authStateSignature, runtimeOptions);
1515
+ await runtimeMetadataWriter(runtimeDir, started);
1516
+ await assertCodexAuthGenerationCurrent(started.authStateSignature, runtimeOptions);
1517
+ } catch (error) {
1518
+ const helper = codexAppServerIsHelper(runtimeOptions);
1519
+ const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
1520
+ helper,
1521
+ executionId: started.executionId,
1522
+ pid: started.pid,
1523
+ processIdentity: started.processIdentity
1524
+ }, runtimeOptions);
1525
+ const failure = new Error(
1526
+ helper
1527
+ ? "Codex isolated helper runtime metadata could not be recorded."
1528
+ : "Codex app-server runtime could not be published safely.",
1529
+ { cause: error }
1530
+ );
1531
+ failure.code = cleanup.cleanupFailed
1532
+ ? (helper
1533
+ ? `${errorPrefix}codex_helper_runtime_cleanup_required`
1534
+ : `${errorPrefix}codex_app_server_cleanup_required`)
1535
+ : (error?.code || (helper
1536
+ ? `${errorPrefix}codex_helper_runtime_metadata_failed`
1537
+ : `${errorPrefix}codex_app_server_metadata_failed`));
1538
+ failure.cleanupRequired = cleanup.cleanupFailed;
1539
+ failure.retryable = false;
1540
+ throw failure;
1541
+ }
1542
+ return {
1543
+ ...started,
1544
+ reused: false
1545
+ };
1546
+ } finally {
1547
+ await releaseLock();
1548
+ }
1549
+ }
1550
+
1551
+ return {
1552
+ ensure: ensureCodexAppServerRuntime,
1553
+ start: startCodexAppServerProcess,
1554
+ stop: stopCodexAppServerRuntime,
1555
+ isLive: codexAppServerMetadataIsLive,
1556
+ cleanupFailedStart: cleanupFailedCodexAppServerStart
1557
+ };
1558
+ }
1559
+