@jskit-ai/assistant-core 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,453 @@
1
+ import { CODEX_APP_SERVER_PROVIDER_ID } from "./codexProcess.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { codexAppServerErrorText } from "./codexEvents.js";
4
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
5
+ import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
6
+
7
+ /** @typedef {import("./codexProvider.js").CodexAppServerAgentProvider} CodexAppServerAgentProvider */
8
+
9
+ export function codexAppServerEndpointForTarget(endpoint = "") {
10
+ const normalizedEndpoint = normalizeAgentText(endpoint);
11
+ if (!normalizedEndpoint) {
12
+ return "";
13
+ }
14
+ return normalizedEndpoint;
15
+ }
16
+
17
+ export function codexTextInput(text = "") {
18
+ return {
19
+ text: String(text ?? ""),
20
+ text_elements: [],
21
+ type: "text"
22
+ };
23
+ }
24
+
25
+ export function codexLocalImageInput(attachments = []) {
26
+ return attachments
27
+ .filter((attachment) => attachment.contentType?.startsWith("image/"))
28
+ .map((attachment) => ({ type: "localImage", path: attachment.path }));
29
+ }
30
+
31
+ export async function sendCodexAppServerPrompt({
32
+ attachments = [],
33
+ clientUserMessageId = "",
34
+ outputSchema = null,
35
+ provider,
36
+ prompt = "",
37
+ threadId = "",
38
+ readOnly = false
39
+ } = {}, authorized = {}) {
40
+ const authoredInput = String(prompt ?? "");
41
+ const preparedInput = authorized.preparedInput;
42
+ if (preparedInput === undefined && !authoredInput.trim()) {
43
+ throw new Error("Codex app-server prompt is empty.");
44
+ }
45
+ const input = preparedInput === undefined ? [authoredInput, ...codexLocalImageInput(attachments)] : preparedInput;
46
+ const turnSettings = {
47
+ ...authorized.turnSettings,
48
+ ...(readOnly
49
+ ? {
50
+ sandboxPolicy: {
51
+ networkAccess: false,
52
+ type: "readOnly"
53
+ }
54
+ }
55
+ : {}),
56
+ ...(normalizeAgentText(clientUserMessageId)
57
+ ? { clientUserMessageId: normalizeAgentText(clientUserMessageId) }
58
+ : {})
59
+ };
60
+ if (outputSchema && typeof outputSchema === "object" && !Array.isArray(outputSchema)) {
61
+ turnSettings.outputSchema = outputSchema;
62
+ }
63
+ const turn = await provider.sendTurn(threadId, input, turnSettings);
64
+ return {
65
+ input,
66
+ turn
67
+ };
68
+ }
69
+
70
+ export function codexTurnInput(input = []) {
71
+ const values = Array.isArray(input) ? input : [input];
72
+ return values.map((item) => {
73
+ if (isRecord(item) && item.type === "localImage" && typeof item.path === "string") {
74
+ return { type: "localImage", path: item.path };
75
+ }
76
+ if (isRecord(item) && item.type === "image" && typeof item.url === "string") {
77
+ return { type: "image", url: item.url };
78
+ }
79
+ if (isRecord(item) && item.type === "text") {
80
+ return codexTextInput(item.text);
81
+ }
82
+ return codexTextInput(item);
83
+ });
84
+ }
85
+
86
+ export function shellQuote(value = "") {
87
+ const text = String(value ?? "");
88
+ if (/^[A-Za-z0-9_./:=@+-]+$/u.test(text)) {
89
+ return text;
90
+ }
91
+ return `'${text.replaceAll("'", "'\"'\"'")}'`;
92
+ }
93
+
94
+ export function codexControlProbeCommand(keys, marker) {
95
+ const source = [
96
+ "const {createHash}=require('node:crypto');",
97
+ `const keys=${JSON.stringify(keys)};`,
98
+ `process.stdout.write(${JSON.stringify(marker)}+createHash('sha256').update(JSON.stringify(keys.map(key=>[key,process.env[key]]))).digest('hex'))`
99
+ ].join("");
100
+ return `${shellQuote(process.execPath)} -e ${shellQuote(source)}`;
101
+ }
102
+
103
+ export function codexCliResumeCommand({
104
+ codexCommand = "",
105
+ endpoint = "",
106
+ threadId = ""
107
+ } = {}) {
108
+ const normalizedEndpoint = codexAppServerEndpointForTarget(endpoint);
109
+ const resolvedCodexCommand = normalizeAgentText(codexCommand) || "codex";
110
+ const normalizedThreadId = normalizeAgentText(threadId);
111
+ if (!normalizedEndpoint) {
112
+ throw new Error("Codex app-server endpoint is required for the native CLI command.");
113
+ }
114
+ if (!normalizedThreadId) {
115
+ throw new Error("Codex thread id is required for the native CLI command.");
116
+ }
117
+ const argv = [
118
+ resolvedCodexCommand,
119
+ "-c",
120
+ "check_for_update_on_startup=false",
121
+ "--remote",
122
+ normalizedEndpoint,
123
+ "resume",
124
+ normalizedThreadId
125
+ ];
126
+ return {
127
+ argv,
128
+ command: argv.map(shellQuote).join(" ")
129
+ };
130
+ }
131
+
132
+ function normalizeAgentTurn(value = {}) {
133
+ const turn = value && typeof value === "object" && !Array.isArray(value) ? value : {};
134
+ return {
135
+ id: normalizeAgentText(turn.id || turn.turnId),
136
+ provider: normalizeAgentText(turn.provider),
137
+ // readable-check: allow no-dual-shape - data: a turn record arrives with Codex's raw turn or is that raw turn
138
+ raw: turn.raw || turn,
139
+ status: normalizeAgentText(turn.status || turn.raw?.status)
140
+ };
141
+ }
142
+
143
+ // The one reading of a sendTurn delivery. Its turn is already normalized; the
144
+ // provider's own error stays on the raw native turn.
145
+ export function codexAppServerDeliveredTurn(delivery) {
146
+ const turn = delivery?.turn || {};
147
+ return {
148
+ turnId: normalizeAgentText(turn.id),
149
+ status: normalizeAgentText(turn.status || turn.raw?.status),
150
+ error: codexAppServerErrorText(turn.raw?.error || turn.response?.turn?.error || turn.error)
151
+ };
152
+ }
153
+ /**
154
+ * The provider's turns: sending, steering and interrupting them, the native commands they start, and the
155
+ * control probes that verify a thread's environment.
156
+ * @param {object} dependencies
157
+ * @param {CodexAppServerAgentProvider} dependencies.provider
158
+ */
159
+ export function createCodexProviderTurns({ provider }) {
160
+ // Native commands a turn started, by thread, turn and item.
161
+ const commandExecutions = new Map();
162
+ // How each interrupted turn ended, by thread and turn.
163
+ const interruptedTurns = new Map();
164
+ // Each thread's pending or verified environment probe.
165
+ const threadControlProbes = new Map();
166
+ let commandStopFailure = null;
167
+
168
+ async function sendTurn(threadId = "", input = [], params = {}, { signal } = {}) {
169
+ const operation = async () => {
170
+ const client = await provider.activeClient();
171
+ return client.request("turn/start", {
172
+ ...params,
173
+ input: codexTurnInput(input),
174
+ threadId: normalizeAgentText(threadId || params.threadId)
175
+ }, { signal });
176
+ };
177
+ const response = provider.options.prepareThreadEnvironment
178
+ ? await provider.withThreadEnvironment(threadId || params.threadId, {}, operation)
179
+ : await provider.runRequest(operation, "codex-app-server-turn-start");
180
+ return {
181
+ ...normalizeAgentTurn({
182
+ id: response?.turn?.id || response?.turnId,
183
+ provider: CODEX_APP_SERVER_PROVIDER_ID,
184
+ // readable-check: allow no-dual-shape - data: Codex replies to a turn request with the turn wrapped or bare
185
+ raw: response?.turn || response
186
+ }),
187
+ response
188
+ };
189
+ }
190
+
191
+ async function steerTurn(threadId = "", turnId = "", input = [], params = {}, { signal } = {}) {
192
+ const client = await provider.activeClient();
193
+ const response = await provider.runRequest(
194
+ () => client.request("turn/steer", {
195
+ ...params,
196
+ expectedTurnId: normalizeAgentText(turnId || params.expectedTurnId),
197
+ input: codexTurnInput(input),
198
+ threadId: normalizeAgentText(threadId || params.threadId)
199
+ }, { signal }),
200
+ "codex-app-server-turn-steer"
201
+ );
202
+ return {
203
+ ...normalizeAgentTurn({
204
+ id: response?.turn?.id || response?.turnId || turnId,
205
+ provider: CODEX_APP_SERVER_PROVIDER_ID,
206
+ // readable-check: allow no-dual-shape - data: Codex replies to a turn request with the turn wrapped or bare
207
+ raw: response?.turn || response
208
+ }),
209
+ response
210
+ };
211
+ }
212
+
213
+ function interruptionOutcome(threadId = "", turnId = "") {
214
+ const key = JSON.stringify([normalizeAgentText(threadId), normalizeAgentText(turnId)]);
215
+ return interruptedTurns.get(key) || "";
216
+ }
217
+
218
+ async function interruptTurn(threadId = "", turnId = "", { outcome = "" } = {}) {
219
+ const client = await provider.activeClient();
220
+ return provider.runRequest(
221
+ () => interruptNativeTurn(client, normalizeAgentText(threadId), normalizeAgentText(turnId), undefined, outcome),
222
+ "codex-app-server-turn-interrupt"
223
+ );
224
+ }
225
+
226
+ async function interruptNativeTurn(client, threadId, turnId, signal, outcome = "") {
227
+ const key = JSON.stringify([threadId, turnId]);
228
+ interruptedTurns.set(key, outcome || interruptedTurns.get(key) || "");
229
+ const result = await client.request("turn/interrupt", { threadId, turnId }, { signal });
230
+ await Promise.all([...commandExecutions.values()]
231
+ .filter((command) => command.threadId === threadId && command.turnId === turnId)
232
+ .map((command) => stopCommandExecution(command)));
233
+ return result;
234
+ }
235
+
236
+ function stopCommandExecution(command) {
237
+ if (command.stopTask) return command.stopTask;
238
+ command.stopTask = (async () => {
239
+ const signal = AbortSignal.timeout(5000);
240
+ const request = (method, params) => command.client.request(method, params, { signal });
241
+ while (!command.completed) {
242
+ signal.throwIfAborted();
243
+ let cursor;
244
+ let terminal;
245
+ const cursors = new Set();
246
+ do {
247
+ const page = await request("thread/backgroundTerminals/list", {
248
+ threadId: command.threadId, limit: 100, ...(cursor ? { cursor } : {})
249
+ });
250
+ if (!Array.isArray(page.data)) throw new Error("Codex returned no command inventory.");
251
+ terminal = page.data.find((entry) => entry.processId === command.processId);
252
+ cursor = page.nextCursor;
253
+ if (cursor && cursors.has(cursor)) throw new Error("Codex repeated its command inventory cursor.");
254
+ cursors.add(cursor);
255
+ } while (!terminal && cursor);
256
+ if (command.completed) return;
257
+ if (terminal) {
258
+ // A recycled process ID must never target a different command.
259
+ if (terminal.itemId !== command.itemId) throw new Error("The native command identity changed during Stop.");
260
+ const stopped = await request("thread/backgroundTerminals/terminate", {
261
+ threadId: command.threadId, processId: command.processId
262
+ });
263
+ if (stopped.terminated === true) return;
264
+ }
265
+ // Codex emits command-start before adding it to the process inventory.
266
+ // Absence during that interval is not proof that the command exited.
267
+ await new Promise((resolve) => setTimeout(resolve, 25));
268
+ }
269
+ })().catch((cause) => {
270
+ const error = Object.assign(new Error("Codex could not confirm that the stopped turn's command exited.", { cause }), {
271
+ code: `${provider.errorPrefix}codex_command_stop_unconfirmed`
272
+ });
273
+ commandStopFailure = error;
274
+ // A failed native control stops work through the lost-observation path, whose owner verifies the
275
+ // runtime stop. That owner may already be awaiting this command while stopping a lost observation,
276
+ // so command cleanup does not wait on its own caller.
277
+ void Promise.resolve(provider.failObservation(error)).catch(() => null);
278
+ throw error;
279
+ });
280
+ return command.stopTask;
281
+ }
282
+
283
+ function nativeCliResumeCommand(threadId = "") {
284
+ const runtime = provider.runtime || {};
285
+ return codexCliResumeCommand({
286
+ codexCommand: provider.options.codexCommand || "codex",
287
+ endpoint: runtime.endpoint,
288
+ threadId
289
+ });
290
+ }
291
+
292
+ async function verifyThreadEnvironment(threadId, environment, request, signal, { requiredKeys = [] } = {}) {
293
+ const keys = Object.keys(environment).filter((key) => key.startsWith(provider.configuration.controlEnvironmentPrefix) || requiredKeys.includes(key)).sort();
294
+ if (!keys.length) return;
295
+ const marker = `${provider.configuration.controlProbePrefix}${randomUUID()}:`;
296
+ const probe = { marker, pending: [], verified: false, turnId: "" };
297
+ threadControlProbes.set(threadId, probe);
298
+ const expected = createHash("sha256").update(JSON.stringify(keys.map((key) => [key, environment[key]]))).digest("hex");
299
+ // This bounded native shell check uses no model and prints only a digest,
300
+ // never credentials or environment values.
301
+ await request("thread/shellCommand", {
302
+ threadId, command: codexControlProbeCommand(keys, marker), timeoutMs: 2000
303
+ });
304
+ while (true) {
305
+ signal.throwIfAborted();
306
+ // The native completion event owns this result; saved history is not a
307
+ // second acknowledgement of this live control operation.
308
+ if (probe.completed && probe.result) {
309
+ if (probe.result.type !== "commandExecution" || probe.result.exitCode !== 0 ||
310
+ probe.result.aggregatedOutput?.trim() !== `${marker}${expected}`) {
311
+ throw Object.assign(new Error("Another native subscriber retained the previous assistant settings. Close the native assistant terminal and retry Resume."), {
312
+ code: "assistant_agent_control_binding_retained"
313
+ });
314
+ }
315
+ return;
316
+ }
317
+ await new Promise((resolve) => setTimeout(resolve, 25));
318
+ }
319
+ }
320
+
321
+ function isControlProbeTurn(threadId, turnId) {
322
+ const probe = threadControlProbes.get(threadId);
323
+ return Boolean(turnId && probe?.verified && probe.turnId === turnId);
324
+ }
325
+
326
+ function recordInterruption(threadId, turnId) {
327
+ const key = JSON.stringify([threadId, turnId]);
328
+ interruptedTurns.set(key, interruptedTurns.get(key) || "control_reconfiguration");
329
+ }
330
+
331
+ // A thread whose control recovery failed publishes every event its unverified probe held back.
332
+ function abandonControlProbe(threadId) {
333
+ const probe = threadControlProbes.get(threadId);
334
+ if (probe && !probe.verified) {
335
+ threadControlProbes.delete(threadId);
336
+ for (const pending of probe.pending) provider.publishNotification(pending);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Tracks the native commands and control probes a notification reports, and says whether subscribers see it:
342
+ * a verified control probe's events, and an unverified probe's held events, are not published.
343
+ */
344
+ function observeNotification(notification) {
345
+ const threadId = notification.params?.threadId;
346
+ const turnId = notification.params?.turnId;
347
+ const item = notification.params?.item;
348
+ if (item?.type === "commandExecution" && item.processId && item.source === "unifiedExecStartup") {
349
+ const key = JSON.stringify([threadId, turnId, item.id]);
350
+ if (notification.method === "item/completed") {
351
+ const command = commandExecutions.get(key);
352
+ if (command) command.completed = true;
353
+ commandExecutions.delete(key);
354
+ } else if (notification.method === "item/started") {
355
+ const command = commandExecutions.get(key) || {
356
+ threadId, turnId, itemId: item.id, processId: item.processId, client: provider.client
357
+ };
358
+ commandExecutions.set(key, command);
359
+ // Native startup may finish after turn/interrupt has already returned.
360
+ if (interruptedTurns.has(JSON.stringify([threadId, turnId]))) {
361
+ void stopCommandExecution(command).catch(() => null);
362
+ }
363
+ }
364
+ }
365
+ const probe = threadControlProbes.get(threadId);
366
+ const method = notification.method;
367
+ if (probe && (/^(turn|item)\//u.test(method) || method === "thread/status/changed")) {
368
+ const turnId = notification.params?.turnId || notification.params?.turn?.id;
369
+ if (method === "turn/started" && probe.turnId && probe.turnId !== turnId) {
370
+ threadControlProbes.delete(threadId);
371
+ } else if (probe.verified) {
372
+ if (turnId === probe.turnId) {
373
+ if (method === "item/completed" && notification.params.item?.id === probe.itemId) {
374
+ probe.result = notification.params.item;
375
+ }
376
+ if (method === "turn/completed") probe.completed = true;
377
+ }
378
+ return false;
379
+ } else {
380
+ if (method === "turn/started") probe.turnId = turnId;
381
+ probe.pending.push(notification);
382
+ if (method === "item/started" || probe.pending.length >= 32) {
383
+ const item = notification.params?.item;
384
+ if (item?.type === "commandExecution" && item.command?.includes(probe.marker)) {
385
+ probe.verified = true;
386
+ probe.itemId = item.id;
387
+ probe.pending = [];
388
+ } else {
389
+ // A concurrent native user turn must retain every event. Only a
390
+ // positively identified control probe may be hidden from chat.
391
+ threadControlProbes.delete(threadId);
392
+ for (const pending of probe.pending) provider.publishNotification(pending);
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+ }
398
+ return true;
399
+ }
400
+
401
+ async function stopThreadForObservationLoss(threadId, turnId) {
402
+ // Control uses only this open socket. It must not spawn or resume work
403
+ // to find out whether the work we lost sight of has stopped.
404
+ const client = provider.client;
405
+ if (!client?.isOpen()) throw new Error("Codex control connection is unavailable.");
406
+ if (commandStopFailure) throw commandStopFailure;
407
+ const signal = AbortSignal.timeout(5_000);
408
+ const request = (method, params) => client.request(method, params, { signal });
409
+ let { goal } = await request("thread/goal/get", { threadId });
410
+ if (goal?.status === "active") {
411
+ const result = await request("thread/goal/set", { threadId, status: "paused" });
412
+ if (result.goal?.status !== "paused" || result.goal?.createdAt !== goal.createdAt || result.goal?.objective !== goal.objective) {
413
+ throw new Error("Codex did not confirm that the same goal was paused.");
414
+ }
415
+ goal = result.goal;
416
+ }
417
+ const readStatus = async () => {
418
+ const { thread } = await request("thread/read", { threadId, includeTurns: false });
419
+ return typeof thread?.status === "string" ? thread.status : thread?.status?.type;
420
+ };
421
+ if (!["idle", "notLoaded"].includes(await readStatus())) {
422
+ if (!turnId) {
423
+ const page = await request("thread/turns/list", { threadId, limit: 1, sortDirection: "desc", itemsView: "summary" });
424
+ turnId = page.data?.[0]?.id;
425
+ }
426
+ if (!turnId) throw new Error("Codex's running turn could not be identified.");
427
+ await interruptNativeTurn(client, threadId, turnId, signal);
428
+ if (await readStatus() !== "idle") throw new Error("Codex did not confirm that its turn stopped.");
429
+ }
430
+ return { goal };
431
+ }
432
+
433
+ function clear() {
434
+ threadControlProbes.clear();
435
+ commandExecutions.clear();
436
+ interruptedTurns.clear();
437
+ }
438
+
439
+ return Object.freeze({
440
+ sendTurn,
441
+ steerTurn,
442
+ interruptionOutcome,
443
+ interruptTurn,
444
+ nativeCliResumeCommand,
445
+ verifyThreadEnvironment,
446
+ isControlProbeTurn,
447
+ recordInterruption,
448
+ abandonControlProbe,
449
+ observeNotification,
450
+ stopThreadForObservationLoss,
451
+ clear
452
+ });
453
+ }
@@ -1,8 +1,17 @@
1
- import { normalizeCodexRunText, codexAppServerTurnCanReceiveProviderActivity, codexAppServerTurnKey } from "./codexTurnState.js";
1
+ import { codexAppServerTurnCanReceiveProviderActivity, codexAppServerTurnKey } from "./codexTurnState.js";
2
2
  import { codexAppServerContentText, codexAppServerNotificationItem, codexAppServerOutputOwnerTurnId, codexAppServerNotificationParams, codexAppServerNotificationTurnId } from "./codexEvents.js";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
4
 
4
- // Original per-turn reasoning channels, persistence queue and cleanup.
5
- // The containing output instance shares these exact Maps with its run owner.
5
+ /**
6
+ * Per-turn reasoning channels, their persistence queue and cleanup. The output owner that builds it shares these Maps
7
+ * with its run owner.
8
+ * @param {object} dependencies
9
+ * @param {import("./codexTurn.js").CodexCreateStore} dependencies.createStore
10
+ * @param {import("./codexTurn.js").CodexPublish} dependencies.publish
11
+ * @param {import("./codexTurn.js").CodexAgentRunTurnStateReader} dependencies.turnStateFromAgentRun
12
+ * @param {import("./codexTurn.js").CodexDebugLog} dependencies.debugLog
13
+ * @param {(store: object, sessionId?: string) => Promise<object | null>} dependencies.readCodexAppServerAgentRunForSession
14
+ */
6
15
  export function createCodexAppServerReasoning({
7
16
  createStore,
8
17
  publish,
@@ -41,7 +50,7 @@ export function createCodexAppServerReasoning({
41
50
  function codexAppServerReasoningSummaryKey(notification = {}) {
42
51
  const params = codexAppServerNotificationParams(notification);
43
52
  const item = codexAppServerNotificationItem(notification);
44
- const itemId = normalizeCodexRunText(params.itemId || item?.id || "summary");
53
+ const itemId = normalizeText(params.itemId || item?.id || "summary");
45
54
  const summaryIndex = String(params.summaryIndex ?? params.contentIndex ?? params.index ?? 0).trim() || "0";
46
55
  return `${itemId}:${summaryIndex}`;
47
56
  }
@@ -63,7 +72,7 @@ export function createCodexAppServerReasoning({
63
72
  }
64
73
 
65
74
  function codexAppServerReasoningSummaryDisplayText(value = "") {
66
- let text = normalizeCodexRunText(value).replace(/\r\n/gu, "\n");
75
+ let text = normalizeText(value).replace(/\r\n/gu, "\n");
67
76
  if (!text) {
68
77
  return "";
69
78
  }
@@ -79,7 +88,7 @@ export function createCodexAppServerReasoning({
79
88
  }
80
89
 
81
90
  function codexAppServerReasoningDisplayText(value = "") {
82
- return normalizeCodexRunText(value)
91
+ return normalizeText(value)
83
92
  .replace(/\r\n/gu, "\n")
84
93
  .split(/\n{2,}/u)
85
94
  .map(codexAppServerReasoningSummaryDisplayText)
@@ -91,28 +100,28 @@ export function createCodexAppServerReasoning({
91
100
  function recordCodexAppServerReasoningNotification(threadId = "", notification = {}, {
92
101
  turnId = ""
93
102
  } = {}) {
94
- const method = normalizeCodexRunText(notification.method);
103
+ const method = normalizeText(notification.method);
95
104
  const item = codexAppServerNotificationItem(notification);
96
105
  const completed = method === "item/completed" && item?.type === "reasoning";
97
106
  if (!completed && !["item/reasoning/summaryPartAdded", "item/reasoning/summaryTextDelta", "item/reasoning/textDelta"].includes(method)) {
98
107
  return false;
99
108
  }
100
- const normalizedThreadId = normalizeCodexRunText(threadId);
101
- const normalizedTurnId = normalizeCodexRunText(turnId) || codexAppServerNotificationTurnId(notification);
109
+ const normalizedThreadId = normalizeText(threadId);
110
+ const normalizedTurnId = normalizeText(turnId) || codexAppServerNotificationTurnId(notification);
102
111
  if (!normalizedThreadId) {
103
112
  return false;
104
113
  }
105
114
  const state = codexAppServerReasoningTurnState(normalizedThreadId, normalizedTurnId);
106
115
  const params = codexAppServerNotificationParams(notification);
107
- const itemId = normalizeCodexRunText(params.itemId || item?.id || "summary");
116
+ const itemId = normalizeText(params.itemId || item?.id || "summary");
108
117
  if (completed) {
109
118
  // Completed-only providers expose the same readable parts in the item.
110
119
  // Streamed items have already been recorded; do not replay their text.
111
120
  if (state.channels.has(itemId)) return false;
112
121
  const summary = codexAppServerContentText(item.summary);
113
- const text = normalizeCodexRunText(summary) ? summary : codexAppServerContentText(item.content);
122
+ const text = normalizeText(summary) ? summary : codexAppServerContentText(item.content);
114
123
  return recordCodexAppServerReasoningNotification(normalizedThreadId, {
115
- method: normalizeCodexRunText(summary) ? "item/reasoning/summaryTextDelta" : "item/reasoning/textDelta",
124
+ method: normalizeText(summary) ? "item/reasoning/summaryTextDelta" : "item/reasoning/textDelta",
116
125
  params: { itemId, delta: text }
117
126
  }, { turnId: normalizedTurnId });
118
127
  }
@@ -147,8 +156,8 @@ export function createCodexAppServerReasoning({
147
156
  }
148
157
 
149
158
  async function recordCodexAppServerReasoningForSession(sessionId = "", threadId = "", notifications = []) {
150
- const normalizedSessionId = normalizeCodexRunText(sessionId);
151
- const normalizedThreadId = normalizeCodexRunText(threadId);
159
+ const normalizedSessionId = normalizeText(sessionId);
160
+ const normalizedThreadId = normalizeText(threadId);
152
161
  const store = await createStore(normalizedSessionId);
153
162
  const run = await readCodexAppServerAgentRunForSession(store, normalizedSessionId);
154
163
  const currentTurn = turnStateFromAgentRun(run || {});
@@ -189,14 +198,14 @@ export function createCodexAppServerReasoning({
189
198
 
190
199
  function codexAppServerReasoningPersistKey(sessionId = "", threadId = "", turnId = "") {
191
200
  return [
192
- normalizeCodexRunText(sessionId),
193
- normalizeCodexRunText(threadId),
194
- normalizeCodexRunText(turnId) || "*"
201
+ normalizeText(sessionId),
202
+ normalizeText(threadId),
203
+ normalizeText(turnId) || "*"
195
204
  ].filter(Boolean).join(":");
196
205
  }
197
206
 
198
207
  async function persistCodexAppServerReasoningSummary(sessionId = "", threadId = "", turnId = "") {
199
- const normalizedSessionId = normalizeCodexRunText(sessionId);
208
+ const normalizedSessionId = normalizeText(sessionId);
200
209
  const state = codexAppServerReasoningExistingTurnState(threadId, turnId);
201
210
  const segments = Array.isArray(state?.segments) ? state.segments : [];
202
211
  const pendingSegments = segments
@@ -210,7 +219,7 @@ export function createCodexAppServerReasoning({
210
219
  }
211
220
  const store = await createStore(normalizedSessionId);
212
221
  const run = await readCodexAppServerAgentRunForSession(store, normalizedSessionId);
213
- if (normalizeCodexRunText(run?.inputSource) === "terminal") {
222
+ if (normalizeText(run?.inputSource) === "terminal") {
214
223
  return;
215
224
  }
216
225
  const turn = turnStateFromAgentRun(run || {});
@@ -221,8 +230,8 @@ export function createCodexAppServerReasoning({
221
230
  currentThreadId: turn.threadId,
222
231
  currentTurnId: turn.turnId,
223
232
  sessionId: normalizedSessionId,
224
- threadId: normalizeCodexRunText(threadId),
225
- turnId: normalizeCodexRunText(turnId)
233
+ threadId: normalizeText(threadId),
234
+ turnId: normalizeText(turnId)
226
235
  });
227
236
  return;
228
237
  }
@@ -302,7 +311,7 @@ export function createCodexAppServerReasoning({
302
311
  return true;
303
312
  }
304
313
 
305
- return {
314
+ return Object.freeze({
306
315
  reasoningTurns: codexAppServerReasoningTurns,
307
316
  reasoningPersistQueues: codexAppServerReasoningPersistQueues,
308
317
  recordReasoningForSession: recordCodexAppServerReasoningForSession,
@@ -310,5 +319,5 @@ export function createCodexAppServerReasoning({
310
319
  flushReasoningPersist: flushCodexAppServerReasoningPersist,
311
320
  cleanupReasoningTurn: cleanupCodexAppServerReasoningTurn,
312
321
  splitReasoningTurn: splitCodexAppServerReasoningTurn
313
- };
322
+ });
314
323
  }
@@ -1,10 +1,23 @@
1
1
  import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage } from "./codexTurnState.js";
2
2
  import { createCodexAppServerDetachedTurnWatcher, waitForCodexAppServerTurn } from "./codexDetachedTurn.js";
3
- import { codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProvider.js";
3
+ import { codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProviderTurns.js";
4
4
  import { resumeExactCodexAppServerThread, startFreshCodexAppServerThread } from "./codexRenewalThreads.js";
5
- import { codexAppServerProviderTurnId, inspectCodexAppServerRenewalThread } from "./codexEvents.js";
5
+ import { codexAppServerProviderTurnId, inspectCodexAppServerRenewalThread } from "./codexThreadSnapshot.js";
6
6
 
7
- // Private composition of unchanged operations from the original run owner.
7
+ /**
8
+ * The run owner's renewal commands: the handover of a running conversation to a renewed runtime, and its seed turn.
9
+ * @param {object} dependencies
10
+ * @param {import("./codexRunJournal.js").CodexRunJournal["claimTurnStart"]} dependencies.claimCodexAppServerTurnStart
11
+ * @param {import("./codexNotificationQueue.js").CodexAppServerNotificationQueue} dependencies.codexAppServerNotificationQueue
12
+ * @param {import("./codexTurnRecovery.js").CodexTurnRecovery["completeTurn"]} dependencies.completeCodexAppServerTurn
13
+ * @param {string} dependencies.errorPrefix
14
+ * @param {import("./codexTurnRecovery.js").CodexTurnRecovery["markTurnActive"]} dependencies.markCodexAppServerTurnActive
15
+ * @param {import("./codexRunJournal.js").CodexRunJournal["markTurnIdle"]} dependencies.markCodexAppServerTurnIdle
16
+ * @param {import("./codexNotificationObserver.js").CodexNotificationObserver} dependencies.observer
17
+ * @param {import("./codexProviderSelection.js").CodexProviderSelection} dependencies.selection
18
+ * @param {string} dependencies.turnAlreadyRunningError
19
+ * @param {import("./codexRunJournal.js").CodexRunJournal["writeUserMessageOwnership"]} dependencies.writeCodexAppServerUserMessageOwnership
20
+ */
8
21
  export function createCodexRenewalCommands({
9
22
  claimCodexAppServerTurnStart,
10
23
  codexAppServerNotificationQueue,
@@ -285,9 +298,8 @@ export function createCodexRenewalCommands({
285
298
  return { result, reconciled, turnId, threadId, freshThread: started.fresh };
286
299
  }
287
300
 
288
-
289
- return {
301
+ return Object.freeze({
290
302
  runCodexAppServerRenewalHandover,
291
303
  runCodexAppServerRenewalSeed
292
- };
304
+ });
293
305
  }