@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,168 @@
1
+ import { createHash } from "node:crypto";
2
+ import { open } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
5
+ import { normalizeNullableText } from "./normalize.js";
6
+
7
+ const CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES = 1024 * 1024;
8
+ const CODEX_APP_SERVER_CHATGPT_ACCESS_TOKEN_MAX_LENGTH = 256 * 1024;
9
+ export const CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH = 512;
10
+ const CODEX_APP_SERVER_PLAN_TYPE_MAX_LENGTH = 128;
11
+ const CODEX_APP_SERVER_API_KEY_MAX_LENGTH = 16 * 1024;
12
+
13
+ /** Native account records and secret-change detection. Hosts select the credential home. */
14
+ export function createCodexAccountReader({
15
+ errorPrefix = "",
16
+ identityNamespace = "codex-account-v1",
17
+ secretNamespace = "codex-auth-secret-v1"
18
+ } = {}) {
19
+ function codexAppServerHelperAuthError(code = "", message = "") {
20
+ const error = new Error(
21
+ normalizeNullableText(message) || "Codex helper authentication is unavailable. Reconnect Codex and retry."
22
+ );
23
+ error.code = normalizeNullableText(code) || `${errorPrefix}codex_helper_auth_unavailable`;
24
+ return error;
25
+ }
26
+
27
+ function boundedCodexAuthText(value, {
28
+ label = "Codex authentication value",
29
+ maxLength = 0
30
+ } = {}) {
31
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
32
+ throw codexAppServerHelperAuthError(
33
+ `${errorPrefix}codex_helper_auth_invalid`,
34
+ `${label} is missing or invalid. Reconnect Codex and retry.`
35
+ );
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function codexAccountIdentitySignature(authMode = "", identity = "") {
41
+ return `sha256:${createHash("sha256")
42
+ .update(`${identityNamespace}\0`, "utf8")
43
+ .update(normalizeNullableText(authMode), "utf8")
44
+ .update("\0", "utf8")
45
+ .update(String(identity || ""), "utf8")
46
+ .digest("hex")}`;
47
+ }
48
+
49
+ function codexAuthSecretSignature(secret = "") {
50
+ return createHash("sha256")
51
+ .update(`${secretNamespace}\0`, "utf8")
52
+ .update(String(secret || ""), "utf8")
53
+ .digest("hex");
54
+ }
55
+
56
+ async function readBoundedCodexAuthJson(filePath = "") {
57
+ let handle = null;
58
+ try {
59
+ handle = await open(filePath, "r");
60
+ const fileStat = await handle.stat();
61
+ if (!fileStat.isFile() || fileStat.size > CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES) {
62
+ throw codexAppServerHelperAuthError(
63
+ `${errorPrefix}codex_helper_auth_invalid`,
64
+ "The selected Codex authentication record is invalid. Reconnect Codex and retry."
65
+ );
66
+ }
67
+ const buffer = Buffer.alloc(CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES + 1);
68
+ let offset = 0;
69
+ while (offset < buffer.length) {
70
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
71
+ if (bytesRead === 0) {
72
+ break;
73
+ }
74
+ offset += bytesRead;
75
+ }
76
+ if (offset > CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES) {
77
+ throw codexAppServerHelperAuthError(
78
+ `${errorPrefix}codex_helper_auth_invalid`,
79
+ "The selected Codex authentication record is too large. Reconnect Codex and retry."
80
+ );
81
+ }
82
+ const parsed = JSON.parse(buffer.subarray(0, offset).toString("utf8"));
83
+ if (!isRecord(parsed)) {
84
+ throw new Error("invalid authentication record");
85
+ }
86
+ return parsed;
87
+ } catch (error) {
88
+ // readable-check: allow optional-call - data: a caught value whose code may be absent or not a string
89
+ if (error?.code?.startsWith?.(`${errorPrefix}codex_helper_auth_`)) {
90
+ throw error;
91
+ }
92
+ throw codexAppServerHelperAuthError(
93
+ `${errorPrefix}codex_helper_auth_unavailable`,
94
+ "The selected Codex authentication record could not be read. Reconnect Codex and retry."
95
+ );
96
+ } finally {
97
+ await handle?.close().catch(() => null);
98
+ }
99
+ }
100
+
101
+ async function readCodexSelectedAccountAuth(options = {}) {
102
+ const toolHomeSource = normalizeNullableText(options.toolHomeSource);
103
+ if (!toolHomeSource || !path.isAbsolute(toolHomeSource)) {
104
+ throw codexAppServerHelperAuthError(
105
+ `${errorPrefix}codex_helper_auth_unavailable`,
106
+ "The selected Codex account is unavailable. Reconnect Codex and retry."
107
+ );
108
+ }
109
+ const auth = await readBoundedCodexAuthJson(path.join(toolHomeSource, ".codex", "auth.json"));
110
+ const storedMode = normalizeNullableText(auth.auth_mode).toLowerCase();
111
+ if (storedMode === "chatgpt") {
112
+ const tokens = isRecord(auth.tokens) ? auth.tokens : {};
113
+ const accountId = boundedCodexAuthText(tokens.account_id, {
114
+ label: "Codex account identity",
115
+ maxLength: CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH
116
+ });
117
+ const accessToken = boundedCodexAuthText(tokens.access_token, {
118
+ label: "Codex access token",
119
+ maxLength: CODEX_APP_SERVER_CHATGPT_ACCESS_TOKEN_MAX_LENGTH
120
+ });
121
+ const planTypeValue = tokens.chatgpt_plan_type ?? tokens.plan_type;
122
+ const planType = planTypeValue === undefined || planTypeValue === null || planTypeValue === ""
123
+ ? ""
124
+ : boundedCodexAuthText(planTypeValue, {
125
+ label: "Codex plan type",
126
+ maxLength: CODEX_APP_SERVER_PLAN_TYPE_MAX_LENGTH
127
+ });
128
+ return Object.freeze({
129
+ accessToken,
130
+ accountId,
131
+ authMode: "chatgpt",
132
+ identitySignature: codexAccountIdentitySignature("chatgpt", accountId),
133
+ planType,
134
+ secretSignature: codexAuthSecretSignature(accessToken)
135
+ });
136
+ }
137
+ if (["apikey", "api_key"].includes(storedMode)) {
138
+ const apiKey = boundedCodexAuthText(auth.OPENAI_API_KEY, {
139
+ label: "Codex API key",
140
+ maxLength: CODEX_APP_SERVER_API_KEY_MAX_LENGTH
141
+ });
142
+ return Object.freeze({
143
+ apiKey,
144
+ authMode: "apiKey",
145
+ identitySignature: codexAccountIdentitySignature("apiKey", apiKey),
146
+ secretSignature: codexAuthSecretSignature(apiKey)
147
+ });
148
+ }
149
+ throw codexAppServerHelperAuthError(
150
+ `${errorPrefix}codex_helper_auth_invalid`,
151
+ "The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
152
+ );
153
+ }
154
+
155
+ return Object.freeze({
156
+ error: codexAppServerHelperAuthError,
157
+ identitySignature: codexAccountIdentitySignature,
158
+ readRecord: readBoundedCodexAuthJson,
159
+ readSelected: readCodexSelectedAccountAuth
160
+ });
161
+ }
162
+
163
+ const CODEX_AUTH_INVALIDATED_PATTERN =
164
+ /\b(?:token_invalidated|refresh_token_invalidated)\b|authentication token has been invalidated|HTTP error:\s*401 Unauthorized|401 Unauthorized/iu;
165
+
166
+ export function codexAuthOutputRequiresReconnect(output = "") {
167
+ return CODEX_AUTH_INVALIDATED_PATTERN.test(String(output || ""));
168
+ }
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { codexAuthOutputRequiresReconnect } from "./codexConfiguration.js";
2
+ import { codexAuthOutputRequiresReconnect } from "./codexAccountReader.js";
3
3
  import {
4
4
  assertExistingDirectory, codexAppServerCommandBaseEnv, codexAppServerCredentialHome,
5
5
  codexAppServerProcessCwd, ensureWritablePrivateDirectory
@@ -30,7 +30,7 @@ function codexAuthPreflightArgs() {
30
30
 
31
31
  async function runCodexAuthPreflight({
32
32
  codexCommand = "codex",
33
- env = process.env,
33
+ env,
34
34
  executionRoot = "",
35
35
  runtimeDir = "",
36
36
  terminalEnv = {},
@@ -86,9 +86,10 @@ async function runCodexAuthPreflight({
86
86
  }
87
87
  }
88
88
 
89
- export async function assertCodexAuthPreflightReady(options = {}, {
89
+ export async function assertCodexAuthPreflightReady(options, {
90
90
  reason = "codex-auth-preflight", execution, credentials
91
- } = {}) {
91
+ }) {
92
+ if (!options?.env) throw new TypeError("assertCodexAuthPreflightReady requires the application's environment, such as process.env.");
92
93
  const result = await runCodexAuthPreflight(options, execution);
93
94
  if (result.ok && !codexAuthOutputRequiresReconnect(result.output)) {
94
95
  return result;
@@ -27,18 +27,23 @@ function codexAppServerRequestAbortedError(method = "") {
27
27
  }
28
28
 
29
29
  function addSocketListener(socket, eventName, handler) {
30
+ // readable-check: allow method-probe - platform object: a WebSocket is an EventTarget in some implementations and an EventEmitter in others
30
31
  if (typeof socket.addEventListener === "function") {
31
32
  socket.addEventListener(eventName, handler);
33
+ // readable-check: allow optional-call - platform object: a WebSocket is an EventTarget in some implementations and an EventEmitter in others
32
34
  return () => socket.removeEventListener?.(eventName, handler);
33
35
  }
36
+ // readable-check: allow method-probe - platform object: a WebSocket is an EventTarget in some implementations and an EventEmitter in others
34
37
  if (typeof socket.on === "function") {
35
38
  socket.on(eventName, handler);
39
+ // readable-check: allow optional-call - platform object: a WebSocket is an EventTarget in some implementations and an EventEmitter in others
36
40
  return () => socket.off?.(eventName, handler) || socket.removeListener?.(eventName, handler);
37
41
  }
38
42
  throw new Error("Unsupported WebSocket implementation.");
39
43
  }
40
44
 
41
45
  function socketMessageText(event, maxBytes = Number.POSITIVE_INFINITY) {
46
+ // readable-check: allow no-dual-shape - platform object: an EventTarget socket delivers a message event, an EventEmitter socket the data itself
42
47
  const data = event?.data ?? event;
43
48
  if (typeof data === "string") {
44
49
  return Buffer.byteLength(data, "utf8") <= maxBytes ? data : null;
@@ -50,11 +55,14 @@ function socketMessageText(event, maxBytes = Number.POSITIVE_INFINITY) {
50
55
  return Buffer.byteLength(text, "utf8") <= maxBytes ? text : null;
51
56
  }
52
57
 
58
+ /** A client without a disconnect listener reports nothing. */
59
+ function ignoreDisconnect() {}
60
+
53
61
  class CodexAppServerJsonRpcClient {
54
62
  constructor({
55
63
  endpoint = "",
56
64
  maxMessageBytes = Number.POSITIVE_INFINITY,
57
- onDisconnect = null,
65
+ onDisconnect = ignoreDisconnect,
58
66
  requestTimeoutMs = CODEX_APP_SERVER_REQUEST_TIMEOUT_MS,
59
67
  WebSocketImpl = WebSocket
60
68
  } = {}) {
@@ -79,6 +87,7 @@ class CodexAppServerJsonRpcClient {
79
87
  if (!this.endpoint) {
80
88
  throw new Error("Codex app-server endpoint is required.");
81
89
  }
90
+ // readable-check: allow method-probe - input validation: an application may supply its own WebSocket implementation
82
91
  if (typeof this.WebSocketImpl !== "function") {
83
92
  throw new Error("A WebSocket implementation is required for Codex app-server.");
84
93
  }
@@ -123,6 +132,7 @@ class CodexAppServerJsonRpcClient {
123
132
  this.connected = false;
124
133
  this.socket = null;
125
134
  }
135
+ // readable-check: allow no-dual-shape - platform object: an EventTarget socket reports an ErrorEvent wrapping the error, an EventEmitter socket the error itself
126
136
  settle(reject, error?.error || error);
127
137
  }));
128
138
  });
@@ -189,7 +199,7 @@ class CodexAppServerJsonRpcClient {
189
199
  return;
190
200
  }
191
201
  const removeAbortListener = () => {
192
- signal?.removeEventListener?.("abort", abort);
202
+ signal?.removeEventListener("abort", abort);
193
203
  };
194
204
  const abort = () => {
195
205
  const pending = this.pendingRequests.get(id);
@@ -213,7 +223,7 @@ class CodexAppServerJsonRpcClient {
213
223
  resolve,
214
224
  timeout
215
225
  });
216
- signal?.addEventListener?.("abort", abort, { once: true });
226
+ signal?.addEventListener("abort", abort, { once: true });
217
227
  try {
218
228
  this.send({
219
229
  id,
@@ -230,6 +240,7 @@ class CodexAppServerJsonRpcClient {
230
240
  }
231
241
 
232
242
  send(payload) {
243
+ // readable-check: allow method-probe - platform object: a WebSocket implementation's socket
233
244
  if (!this.isOpen() || typeof this.socket.send !== "function") {
234
245
  throw new Error("Codex app-server connection is not open.");
235
246
  }
@@ -264,7 +275,7 @@ class CodexAppServerJsonRpcClient {
264
275
  return;
265
276
  }
266
277
  clearTimeout(pending.timeout);
267
- pending.removeAbortListener?.();
278
+ pending.removeAbortListener();
268
279
  this.pendingRequests.delete(message.id);
269
280
  if (message.error) {
270
281
  const error = new Error(message.error.message || `Codex app-server request failed: ${pending.method}`);
@@ -316,7 +327,7 @@ class CodexAppServerJsonRpcClient {
316
327
  rejectPendingRequests(error) {
317
328
  for (const pending of this.pendingRequests.values()) {
318
329
  clearTimeout(pending.timeout);
319
- pending.removeAbortListener?.();
330
+ pending.removeAbortListener();
320
331
  pending.reject(error);
321
332
  }
322
333
  this.pendingRequests.clear();
@@ -325,7 +336,7 @@ class CodexAppServerJsonRpcClient {
325
336
  disconnect(error) {
326
337
  this.rejectPendingRequests(error);
327
338
  this.close();
328
- this.onDisconnect?.(error);
339
+ this.onDisconnect(error);
329
340
  }
330
341
 
331
342
  close() {
@@ -333,6 +344,7 @@ class CodexAppServerJsonRpcClient {
333
344
  const socket = this.socket;
334
345
  this.connected = false;
335
346
  this.socket = null;
347
+ // readable-check: allow optional-call - platform object: a WebSocket implementation's socket
336
348
  socket?.close?.();
337
349
  }
338
350
  }