@jskit-ai/assistant-core 0.2.6 → 0.2.8

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 +18 -12
  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
@@ -0,0 +1,426 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+ import WebSocket from "ws";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
6
+ import { CodexAppServerJsonRpcClient, socketPathFromCodexAppServerEndpoint } from "./codexClient.js";
7
+ import { normalizeNullableText } from "./normalize.js";
8
+ import {
9
+ CODEX_APP_SERVER_DEFAULT_ENVIRONMENT, CODEX_APP_SERVER_ENDPOINT_STATUS, CODEX_APP_SERVER_EXECUTION_MODES,
10
+ CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS, CODEX_APP_SERVER_METADATA_SCHEMA_VERSION, CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS,
11
+ CODEX_APP_SERVER_PROCESS_STATE, CODEX_APP_SERVER_PROVIDER_ID, CODEX_APP_SERVER_READY_TIMEOUT_MS,
12
+ CODEX_APP_SERVER_RUNTIME_STATUS, CODEX_APP_SERVER_TRANSPORT, codexAppServerExecutionMode,
13
+ codexAppServerHelperWorkspaceDir, codexAppServerIsHelper, codexAppServerLivenessTimeoutMs,
14
+ codexAppServerTerminalEnvHash, codexHistoryAdapterUrlIsValid, ensureWritablePrivateDirectory, fileExists,
15
+ normalizeCodexAppServerMetadata, readCodexAppServerMetadata, writeCodexAppServerMetadata
16
+ } from "./codexProcess.js";
17
+ import { createCodexAppServerRuntimeStop } from "./codexRuntimeStop.js";
18
+ import { createCodexAppServerProcessStart } from "./codexRuntimeStart.js";
19
+ import { createConversationProcessIdentity } from "./processIdentity.js";
20
+
21
+ /**
22
+ * The host's process facility. Managed hosts add prepare, stopOwned and authoritative to the local one.
23
+ * @typedef {object} CodexRuntimeExecution
24
+ * @property {(request: object) => Promise<object>} run
25
+ * @property {(executionId: string, options: object) => Promise<object>} stop
26
+ * @property {(owner: object, options: object) => Promise<object>} [stopOwned]
27
+ * @property {() => Promise<void>} [prepare]
28
+ * @property {boolean} [authoritative]
29
+ */
30
+
31
+ /** @typedef {ReturnType<typeof createConversationProcessIdentity>} CodexProcessIdentity */
32
+
33
+ /**
34
+ * The host's account facility for one native runtime.
35
+ * @typedef {object} CodexRuntimeCredentials
36
+ * @property {(options: object) => Promise<string>} identity
37
+ * @property {(options: object) => Promise<string>} generation
38
+ * @property {(captured: string) => Promise<void>} assertCurrent
39
+ * @property {(output: string) => void} observeFailure
40
+ */
41
+
42
+ /**
43
+ * What the process start reads from the runtime configuration.
44
+ * @typedef {object} CodexRuntimeSettings
45
+ * @property {string | undefined} commandHookCommand
46
+ * @property {string} errorPrefix
47
+ * @property {string} modelCatalogEnvironmentName
48
+ * @property {string} processLabel
49
+ * @property {string} processPath
50
+ * @property {string} runtimeTokenEnvironmentName
51
+ * @property {string} commandHashEnvironmentName
52
+ * @property {string} socketPathHelp
53
+ * @property {string | null} umask
54
+ */
55
+
56
+ // One preparation at a time per runtime directory, across every runtime in this
57
+ // process: a second start would race the first for the same socket and lock.
58
+ const codexAppServerRuntimePreparations = new Map();
59
+
60
+ // The process identity a runtime caller's own liveness check reports for the process group.
61
+ function codexAppServerLivenessIdentity(pid, alive) {
62
+ return {
63
+ processGroupIds: alive ? [pid] : [],
64
+ status: alive ? CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.EXACT : CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.ABSENT
65
+ };
66
+ }
67
+
68
+ /**
69
+ * The production shared runtime: one process owner per stable runtime directory.
70
+ * Execution and credential facilities retain host policy; threads do not own this process.
71
+ * @param {object} dependencies
72
+ * @param {CodexRuntimeExecution} dependencies.execution
73
+ * @param {CodexRuntimeCredentials} dependencies.credentials
74
+ * @param {object} [dependencies.configuration]
75
+ */
76
+ export function createCodexAppServerRuntime({ execution, credentials, configuration = {} }) {
77
+ const {
78
+ clientInfo = { name: "jskit", title: "JSKIT", version: "0.1.0" },
79
+ commandHookCommand,
80
+ errorPrefix = "",
81
+ modelCatalogEnvironmentName = CODEX_APP_SERVER_DEFAULT_ENVIRONMENT.modelCatalogSource,
82
+ processLabel = "jskit-codex-app-server",
83
+ processPath = fileURLToPath(new URL("./codexProcessLeader.js", import.meta.url)),
84
+ runtimeTokenEnvironmentName = CODEX_APP_SERVER_DEFAULT_ENVIRONMENT.runtimeToken,
85
+ commandHashEnvironmentName = CODEX_APP_SERVER_DEFAULT_ENVIRONMENT.commandHash,
86
+ socketPathHelp = "Configure a shorter host runtime directory.",
87
+ umask = "0007"
88
+ } = configuration;
89
+ const codexProcessIdentity = createConversationProcessIdentity({
90
+ runtimeTokenEnvironmentName, commandHashEnvironmentName,
91
+ label: "Codex app-server",
92
+ unavailableCode: `${errorPrefix}codex_app_server_process_identity_unavailable`
93
+ });
94
+ const runtimeStop = createCodexAppServerRuntimeStop({ execution, codexProcessIdentity, errorPrefix });
95
+ const processStart = createCodexAppServerProcessStart({
96
+ execution,
97
+ credentials,
98
+ codexProcessIdentity,
99
+ runtimeStop,
100
+ codexAppServerEndpointStatus,
101
+ waitForCodexAppServer,
102
+ settings: {
103
+ commandHookCommand,
104
+ errorPrefix,
105
+ modelCatalogEnvironmentName,
106
+ processLabel,
107
+ processPath,
108
+ runtimeTokenEnvironmentName,
109
+ commandHashEnvironmentName,
110
+ socketPathHelp,
111
+ umask
112
+ }
113
+ });
114
+
115
+ function codexAppServerMetadataIsWellFormed(metadata = {}, options = {}) {
116
+ const expectedAttachmentHostRoot = options.compatibility.attachmentHostRoot;
117
+ const expectedAccountIdentitySignature = normalizeNullableText(options.accountIdentitySignature);
118
+ const expectedExecutionMode = codexAppServerExecutionMode(options);
119
+ const expectedToolHomeSource = codexAppServerIsHelper(options) ? "" : normalizeNullableText(options.toolHomeSource);
120
+ const expectedTerminalEnvHash = codexAppServerTerminalEnvHash(codexAppServerIsHelper(options) ? {} : options.terminalEnv);
121
+ const expectedRuntimesHash = options.compatibility.runtimesHash;
122
+ const expectedExecutionContextHash = options.compatibility.executionContextHash;
123
+ const expectedHelperProcessCwd = expectedExecutionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
124
+ ? codexAppServerHelperWorkspaceDir(metadata.runtimeDir)
125
+ : "";
126
+ return Boolean(
127
+ metadata.schemaVersion === CODEX_APP_SERVER_METADATA_SCHEMA_VERSION &&
128
+ metadata.accountIdentitySignature === expectedAccountIdentitySignature &&
129
+ metadata.attachmentHostRoot === expectedAttachmentHostRoot &&
130
+ metadata.authStateSignature &&
131
+ metadata.executionId &&
132
+ metadata.executionContextHash === expectedExecutionContextHash &&
133
+ metadata.executionMode === expectedExecutionMode &&
134
+ metadata.processCwd &&
135
+ runtimeStop.processMetadataIsIdentifiable(metadata) &&
136
+ metadata.processState === CODEX_APP_SERVER_PROCESS_STATE.RUNNING &&
137
+ (expectedExecutionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER ||
138
+ codexHistoryAdapterUrlIsValid(metadata.historyAdapterBaseUrl, metadata.processIdentity.runtimeToken)) &&
139
+ (!expectedHelperProcessCwd || metadata.processCwd === expectedHelperProcessCwd) &&
140
+ metadata.provider === CODEX_APP_SERVER_PROVIDER_ID &&
141
+ metadata.runtimesHash === expectedRuntimesHash &&
142
+ metadata.terminalEnvHash === expectedTerminalEnvHash &&
143
+ metadata.toolHomeSource === expectedToolHomeSource &&
144
+ metadata.transport === CODEX_APP_SERVER_TRANSPORT.UNIX &&
145
+ metadata.endpoint &&
146
+ metadata.socketPath
147
+ );
148
+ }
149
+
150
+ async function codexAppServerEndpointStatus(endpoint = "", {
151
+ timeoutMs = CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS,
152
+ WebSocketImpl = WebSocket
153
+ } = {}) {
154
+ const normalizedEndpoint = normalizeNullableText(endpoint);
155
+ const socketPath = socketPathFromCodexAppServerEndpoint(normalizedEndpoint);
156
+ if (!socketPath || !await fileExists(socketPath)) {
157
+ return {
158
+ status: CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING
159
+ };
160
+ }
161
+ const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, { fallback: CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS });
162
+ return {
163
+ status: await probeCodexAppServer(normalizedEndpoint, normalizedTimeoutMs, WebSocketImpl)
164
+ };
165
+ }
166
+
167
+ // One bounded connect-and-initialize handshake: responsive, timeout or unreachable.
168
+ async function probeCodexAppServer(endpoint, timeoutMs, WebSocketImpl) {
169
+ const client = new CodexAppServerJsonRpcClient({
170
+ endpoint,
171
+ requestTimeoutMs: timeoutMs,
172
+ WebSocketImpl
173
+ });
174
+ let timeout = null;
175
+ const probe = (async () => {
176
+ await client.connect();
177
+ await client.initialize({ clientInfo });
178
+ return CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE;
179
+ })();
180
+ probe.catch(() => null);
181
+ try {
182
+ return await Promise.race([
183
+ probe,
184
+ new Promise((resolve) => {
185
+ timeout = setTimeout(() => resolve(CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT), timeoutMs);
186
+ timeout.unref();
187
+ })
188
+ ]);
189
+ } catch {
190
+ return CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE;
191
+ } finally {
192
+ clearTimeout(timeout);
193
+ client.close();
194
+ }
195
+ }
196
+
197
+ async function codexAppServerMetadataIsLive(metadata = {}, options = {}) {
198
+ return (await codexAppServerRuntimeStatus(metadata, options)).status === CODEX_APP_SERVER_RUNTIME_STATUS.LIVE;
199
+ }
200
+
201
+ async function codexAppServerRuntimeStatus(metadata = {}, options = {}) {
202
+ const normalized = normalizeCodexAppServerMetadata(metadata);
203
+ if (
204
+ runtimeStop.processMetadataIsIdentifiable(normalized) &&
205
+ normalized.processState === CODEX_APP_SERVER_PROCESS_STATE.STOPPED &&
206
+ normalized.processExitVerifiedAt
207
+ ) {
208
+ return {
209
+ metadata: normalized,
210
+ replace: true,
211
+ reusable: false,
212
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.EXITED
213
+ };
214
+ }
215
+ if (!codexAppServerMetadataIsWellFormed(normalized, options)) {
216
+ return {
217
+ metadata: normalized,
218
+ replace: true,
219
+ reusable: false,
220
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.INCOMPATIBLE
221
+ };
222
+ }
223
+ const authStateSignature = await credentials.generation(options);
224
+ if (normalized.authStateSignature !== authStateSignature) {
225
+ return {
226
+ metadata: normalized,
227
+ replace: true,
228
+ reusable: false,
229
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.INCOMPATIBLE
230
+ };
231
+ }
232
+ // A runtime caller may check its process group's liveness itself; otherwise the process identity is inspected.
233
+ const processIdentity = options.processGroupIsAlive
234
+ ? codexAppServerLivenessIdentity(normalized.pid, options.processGroupIsAlive(normalized.pid))
235
+ : await runtimeStop.inspectProcessIdentity(normalized, options);
236
+ if (
237
+ processIdentity.status === CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.ABSENT ||
238
+ processIdentity.status === CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.MISMATCH
239
+ ) {
240
+ return {
241
+ metadata: normalized,
242
+ replace: true,
243
+ reusable: false,
244
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.EXITED
245
+ };
246
+ }
247
+ if (processIdentity.status !== CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.EXACT) {
248
+ return {
249
+ metadata: normalized,
250
+ replace: false,
251
+ reusable: true,
252
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.SUSPECT
253
+ };
254
+ }
255
+ const endpoint = await codexAppServerEndpointStatus(normalized.endpoint, {
256
+ timeoutMs: codexAppServerLivenessTimeoutMs(options),
257
+ WebSocketImpl: options.WebSocketImpl
258
+ });
259
+ if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE) {
260
+ return {
261
+ metadata: normalized,
262
+ replace: false,
263
+ reusable: true,
264
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.LIVE
265
+ };
266
+ }
267
+ if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING) {
268
+ return {
269
+ metadata: normalized,
270
+ replace: true,
271
+ reusable: false,
272
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.MISSING
273
+ };
274
+ }
275
+ if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT) {
276
+ return {
277
+ metadata: normalized,
278
+ replace: false,
279
+ reusable: true,
280
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.SUSPECT
281
+ };
282
+ }
283
+ return {
284
+ metadata: normalized,
285
+ replace: true,
286
+ reusable: false,
287
+ status: CODEX_APP_SERVER_RUNTIME_STATUS.MISSING
288
+ };
289
+ }
290
+
291
+ async function waitForCodexAppServer(endpoint = "", {
292
+ timeoutMs = CODEX_APP_SERVER_READY_TIMEOUT_MS,
293
+ WebSocketImpl = WebSocket
294
+ } = {}) {
295
+ const socketPath = socketPathFromCodexAppServerEndpoint(endpoint);
296
+ if (!socketPath) {
297
+ return false;
298
+ }
299
+ const startedAt = Date.now();
300
+ while (!await fileExists(socketPath) && Date.now() - startedAt <= timeoutMs) {
301
+ await delay(100);
302
+ }
303
+ const remainingMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
304
+ if (!await fileExists(socketPath)) {
305
+ return false;
306
+ }
307
+ return await probeCodexAppServer(endpoint, remainingMs, WebSocketImpl) === CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE;
308
+ }
309
+
310
+ async function ensureCodexAppServerRuntime(options = {}) {
311
+ const runtimeDir = path.resolve(options.runtimeDir);
312
+ while (codexAppServerRuntimePreparations.has(runtimeDir)) {
313
+ await codexAppServerRuntimePreparations.get(runtimeDir);
314
+ }
315
+ // Each waiter rechecks its own auth and runtime configuration after startup.
316
+ const operation = prepareCodexAppServerRuntime({ ...options, runtimeDir });
317
+ codexAppServerRuntimePreparations.set(runtimeDir, operation);
318
+ try {
319
+ return await operation;
320
+ } finally {
321
+ codexAppServerRuntimePreparations.delete(runtimeDir);
322
+ }
323
+ }
324
+
325
+ async function prepareCodexAppServerRuntime(options) {
326
+ const { runtimeDir } = options;
327
+ const [accountIdentitySignature, authStateSignature] = await Promise.all([
328
+ credentials.identity(options),
329
+ credentials.generation(options)
330
+ ]);
331
+ const runtimeOptions = {
332
+ ...options,
333
+ accountIdentitySignature,
334
+ authStateSignature
335
+ };
336
+ const { runtimeMetadataWriter = writeCodexAppServerMetadata } = options;
337
+ await ensureWritablePrivateDirectory(runtimeDir);
338
+
339
+ const existing = await readCodexAppServerMetadata(runtimeDir);
340
+ const existingStatus = existing ? await codexAppServerRuntimeStatus(existing, runtimeOptions) : null;
341
+ if (existingStatus?.reusable) {
342
+ return {
343
+ ...existingStatus.metadata,
344
+ reused: true,
345
+ runtimeStatus: existingStatus.status
346
+ };
347
+ }
348
+
349
+ const releaseLock = await runtimeStop.acquireRuntimeLock(runtimeDir, runtimeOptions);
350
+ try {
351
+ let socketOwnerDrained = false;
352
+ const afterLock = await readCodexAppServerMetadata(runtimeDir);
353
+ const afterLockStatus = afterLock ? await codexAppServerRuntimeStatus(afterLock, runtimeOptions) : null;
354
+ if (afterLockStatus?.reusable) {
355
+ return {
356
+ ...afterLockStatus.metadata,
357
+ reused: true,
358
+ runtimeStatus: afterLockStatus.status
359
+ };
360
+ }
361
+
362
+ if (afterLock && (!afterLockStatus || afterLockStatus.replace !== false)) {
363
+ const stopped = await runtimeStop.stopProcess(runtimeDir, runtimeOptions);
364
+ if (stopped.processExitVerified !== true) {
365
+ const error = new Error(
366
+ `${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.`
367
+ );
368
+ error.code = `${errorPrefix}codex_app_server_process_identity_unverified`;
369
+ error.cleanupRequired = true;
370
+ error.retryable = false;
371
+ throw error;
372
+ }
373
+ socketOwnerDrained = true;
374
+ }
375
+
376
+ const started = await processStart.start({
377
+ ...runtimeOptions,
378
+ runtimeDir,
379
+ socketOwnerDrained
380
+ });
381
+ try {
382
+ await credentials.assertCurrent(started.authStateSignature, runtimeOptions);
383
+ await runtimeMetadataWriter(runtimeDir, started);
384
+ await credentials.assertCurrent(started.authStateSignature, runtimeOptions);
385
+ } catch (error) {
386
+ const helper = codexAppServerIsHelper(runtimeOptions);
387
+ const cleanup = await runtimeStop.cleanupFailedStart(runtimeDir, {
388
+ helper,
389
+ executionId: started.executionId,
390
+ pid: started.pid,
391
+ processIdentity: started.processIdentity
392
+ }, runtimeOptions);
393
+ const failure = new Error(
394
+ helper
395
+ ? "Codex isolated helper runtime metadata could not be recorded."
396
+ : "Codex app-server runtime could not be published safely.",
397
+ { cause: error }
398
+ );
399
+ failure.code = cleanup.cleanupFailed
400
+ ? (helper
401
+ ? `${errorPrefix}codex_helper_runtime_cleanup_required`
402
+ : `${errorPrefix}codex_app_server_cleanup_required`)
403
+ : (error?.code || (helper
404
+ ? `${errorPrefix}codex_helper_runtime_metadata_failed`
405
+ : `${errorPrefix}codex_app_server_metadata_failed`));
406
+ failure.cleanupRequired = cleanup.cleanupFailed;
407
+ failure.retryable = false;
408
+ throw failure;
409
+ }
410
+ return {
411
+ ...started,
412
+ reused: false
413
+ };
414
+ } finally {
415
+ await releaseLock();
416
+ }
417
+ }
418
+
419
+ return Object.freeze({
420
+ ensure: ensureCodexAppServerRuntime,
421
+ start: processStart.start,
422
+ stop: runtimeStop.stopRuntime,
423
+ isLive: codexAppServerMetadataIsLive,
424
+ cleanupFailedStart: runtimeStop.cleanupFailedStart
425
+ });
426
+ }
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import {
4
+ CODEX_APP_SERVER_PROCESS_STATE,
5
+ readCodexAppServerMetadata,
6
+ removeCodexAppServerRuntimeDir
7
+ } from "./codexProcess.js";
8
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
9
+
10
+ // Every provider owner in this process shares one inventory: one native
11
+ // runtime may serve several owners, so recovery and sharing always inspect the
12
+ // complete inventory by native runtime identity. Caller-local provider keys are
13
+ // grouped by owner without duplicating it.
14
+ export const codexAppServerProviders = new Map();
15
+ export const codexAppServerOwnedRuntimes = new Map();
16
+ const codexAppServerRuntimeLifecycles = new Map();
17
+ // Runtimes an account change left running a turn, by native runtime identity.
18
+ export const codexAppServerStaleRuntimes = new Map();
19
+
20
+ export function codexAppServerOwnedRuntimeKey(providerKey = "", providerOptions) {
21
+ const runtimeDir = normalizeText(providerOptions?.runtimeDir);
22
+ return runtimeDir
23
+ ? `runtime:${path.resolve(runtimeDir)}`
24
+ : `provider:${normalizeText(providerKey)}`;
25
+ }
26
+
27
+ export function withRuntimeLifecycle(runtimeKey, operation) {
28
+ const previous = codexAppServerRuntimeLifecycles.get(runtimeKey) || Promise.resolve();
29
+ const run = previous.catch(() => null).then(operation);
30
+ const tail = run.catch(() => null).finally(() => {
31
+ if (codexAppServerRuntimeLifecycles.get(runtimeKey) === tail) {
32
+ codexAppServerRuntimeLifecycles.delete(runtimeKey);
33
+ }
34
+ });
35
+ codexAppServerRuntimeLifecycles.set(runtimeKey, tail);
36
+ return run;
37
+ }
38
+
39
+ // A session recorded a new runtime directory (a new sign-in gets its own), so
40
+ // its previous one is no longer used. It goes once its runtime has verifiably
41
+ // stopped; a stale runtime still finishing its turn drops it when it stops.
42
+ // Helper cleanup treats an absent directory as retired, which is also what it
43
+ // does with a previous sign-in's runtime it finds stopped.
44
+ export function releaseSupersededCodexRuntime(runtimeDir = "") {
45
+ const runtimeKey = codexAppServerOwnedRuntimeKey("", { runtimeDir });
46
+ return withRuntimeLifecycle(runtimeKey, async () => {
47
+ const stale = codexAppServerStaleRuntimes.get(runtimeKey);
48
+ if (stale) {
49
+ stale.superseded = true;
50
+ return { deferred: true, removed: false };
51
+ }
52
+ if (codexAppServerOwnedRuntimes.has(runtimeKey) || runtimePeers(runtimeKey).length) {
53
+ return { removed: false };
54
+ }
55
+ const metadata = await readCodexAppServerMetadata(runtimeDir);
56
+ if (metadata?.processState !== CODEX_APP_SERVER_PROCESS_STATE.STOPPED || !metadata.processExitVerifiedAt) {
57
+ return { removed: false };
58
+ }
59
+ return { removed: await removeCodexAppServerRuntimeDir(runtimeDir) };
60
+ });
61
+ }
62
+
63
+ export function codexAppServerRuntimeStopWasVerified(result) {
64
+ return result?.stopped === true ||
65
+ result?.processExitVerified === true ||
66
+ result?.runtimeDirRemoved === true;
67
+ }
68
+
69
+ export async function directoryExists(filePath = "") {
70
+ try {
71
+ return (await stat(filePath)).isDirectory();
72
+ } catch (error) {
73
+ if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
74
+ return false;
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ export function* providerRecords() {
81
+ for (const records of codexAppServerProviders.values()) yield* records.values();
82
+ }
83
+
84
+ // Every owner's provider records on one native runtime.
85
+ export function runtimePeers(runtimeKey) {
86
+ return [...providerRecords()].filter(record =>
87
+ codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions) === runtimeKey);
88
+ }
89
+
90
+ export function nativeRequest(client, method, params) {
91
+ return client.request(method, params, { signal: AbortSignal.timeout(5_000) });
92
+ }
93
+
94
+ // The loaded threads of one native runtime that have a running turn, read from
95
+ // the runtime itself so Temporary, Main and native terminal turns all count.
96
+ export async function runningNativeThreads(peers) {
97
+ const client = peers.find(record => record.provider.client?.isOpen())?.provider.client;
98
+ if (!client) return [];
99
+ const running = [];
100
+ const seen = new Set();
101
+ let cursor;
102
+ do {
103
+ const page = await nativeRequest(client, "thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
104
+ for (const threadId of Array.isArray(page?.data) ? page.data : []) {
105
+ const { thread } = await nativeRequest(client, "thread/read", { threadId, includeTurns: false });
106
+ if ((typeof thread?.status === "string" ? thread.status : thread?.status?.type) === "active") running.push(threadId);
107
+ }
108
+ cursor = page?.nextCursor;
109
+ if (cursor && seen.has(cursor)) throw new Error("Codex's loaded-thread inventory did not finish.");
110
+ if (cursor) seen.add(cursor);
111
+ } while (cursor);
112
+ return running;
113
+ }
@@ -1,9 +1,29 @@
1
- import { normalizeCodexRunText as normalizeText } from "./codexTurnState.js";
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
2
 
3
- // Shutdown, wellbeing timers and managed-session records for shared Codex
4
- // runtimes. It works on the run/provider owners' collections; it owns no servers.
5
- // `pendingWork` names the run owner's in-flight task collections that shutdown
6
- // drains; `observer` owns the event subscriptions a released provider ends.
3
+ /** @typedef {ReturnType<typeof import("./codexHelperThreads.js").createCodexHelperThreadLifecycle>} CodexHelperThreadLifecycle */
4
+ /** @typedef {ReturnType<typeof import("./codexNotificationObserver.js").createCodexNotificationObserver>} CodexNotificationObserver */
5
+ /** @typedef {ReturnType<typeof createCodexRuntimeLifecycle>} CodexRuntimeLifecycle */
6
+ /** @typedef {ReturnType<typeof import("./codexTurn.js").createCodexAppServerRunOwner>} CodexRunOwner */
7
+ /** @typedef {(event: string, details: object) => void} CodexDebugLog */
8
+
9
+ /**
10
+ * Shutdown, wellbeing timers and managed-session records for shared Codex
11
+ * runtimes. It works on the run/provider owners' collections; it owns no servers.
12
+ * `pendingWork` names the run owner's in-flight task collections that shutdown
13
+ * drains; `observer` owns the event subscriptions a released provider ends.
14
+ * @param {object} dependencies
15
+ * @param {Record<string, Map<unknown, Promise<unknown>> | Set<Promise<unknown>>>} dependencies.pendingWork
16
+ * @param {CodexNotificationObserver} dependencies.observer
17
+ * @param {(providerKey: string, managed: object) => Promise<unknown>} dependencies.checkManagedConnection
18
+ * @param {import("./codexProviderOwner.js").CodexProviderSessions} dependencies.providerSessions
19
+ * @param {CodexHelperThreadLifecycle | null} dependencies.helperLifecycle
20
+ * @param {() => unknown} dependencies.captureContext
21
+ * @param {<T>(context: unknown, operation: () => T) => T} dependencies.runInContext
22
+ * @param {((sessionId: string, options: { threadId: string }) => Promise<unknown>) | null} [dependencies.continueGoalAfterAccountChange]
23
+ * @param {CodexDebugLog} dependencies.debugLog
24
+ * @param {(error: unknown) => unknown} dependencies.debugError
25
+ * @param {{ message: string, code: string, retryable: boolean } | null} [dependencies.closingError]
26
+ */
7
27
  export function createCodexRuntimeLifecycle({
8
28
  pendingWork,
9
29
  observer,
@@ -12,7 +32,7 @@ export function createCodexRuntimeLifecycle({
12
32
  helperLifecycle,
13
33
  captureContext,
14
34
  runInContext,
15
- continueGoalAfterAccountChange = null,
35
+ continueGoalAfterAccountChange,
16
36
  debugLog,
17
37
  debugError,
18
38
  closingError = null
@@ -268,7 +288,7 @@ export function createCodexRuntimeLifecycle({
268
288
  }
269
289
  });
270
290
  }, providerSessions.wellbeingMs);
271
- timer.unref?.();
291
+ timer.unref();
272
292
  codexAppServerWellbeingTimers.set(normalizedProviderKey, timer);
273
293
  }
274
294
 
@@ -296,7 +316,7 @@ export function createCodexRuntimeLifecycle({
296
316
  }
297
317
 
298
318
  function invalidateRuntimes(input = {}, preparation) {
299
- const serverShutdown = normalizeText(input?.reason) === "server-shutdown";
319
+ const serverShutdown = normalizeText(input.reason) === "server-shutdown";
300
320
  if (serverShutdown) {
301
321
  beginCodexAppServerShutdown();
302
322
  }
@@ -328,18 +348,24 @@ export function createCodexRuntimeLifecycle({
328
348
  })();
329
349
  }
330
350
 
331
- return {
351
+ function nextReconcileGeneration() { return ++codexAppServerThreadReconcileGeneration; }
352
+
353
+ const managedSession = providerKey => providerSessions.managed.get(providerKey);
354
+
355
+ const managedThreadId = providerKey => providerSessions.managed.get(providerKey)?.threadId;
356
+
357
+ return Object.freeze({
332
358
  get closing() { return codexAppServerServerClosing; },
333
359
  assertOpen,
334
360
  get reconcileGeneration() { return codexAppServerThreadReconcileGeneration; },
335
- nextReconcileGeneration() { return ++codexAppServerThreadReconcileGeneration; },
361
+ nextReconcileGeneration,
336
362
  reconcileTasks: codexAppServerReconcileTasks,
337
363
  sessionClosures: codexAppServerSessionClosures,
338
364
  invalidateRuntimes,
339
365
  resources: codexAppServerProviderResources,
340
366
  retireProvider: retireAndCloseCodexAppServerProvider,
341
367
  rememberManagedSession: rememberCodexAppServerManagedSession,
342
- managedSession: providerKey => providerSessions.managed.get(providerKey),
343
- managedThreadId: providerKey => providerSessions.managed.get(providerKey)?.threadId
344
- };
368
+ managedSession,
369
+ managedThreadId
370
+ });
345
371
  }