@jskit-ai/assistant-core 0.1.199 → 0.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,332 @@
1
+ import { normalizeText, isPlainObject } from "./normalize.js";
2
+ import { classifyCodexAppServerEvent, codexAppServerNotificationParams, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerNotificationError, codexAppServerStatusFromValue, codexAppServerProviderThreadAssistantSegments, codexAppServerProviderTurnId, codexAppServerProviderTurnStatus, codexAppServerProviderTurnForOperation, codexAppServerProviderTurnText, codexAppServerProviderTurnError } from "./codexEvents.js";
3
+ import { codexAppServerTurnStatusIsProviderFailure, codexAppServerTurnStatusIsSuccessfulComplete } from "./codexTurnState.js";
4
+
5
+ const CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS = 180_000;
6
+ const CODEX_APP_SERVER_DETACHED_FAILURE_DETAIL_GRACE_MS = 500;
7
+
8
+ function codexAppServerTurnTokenUsage(notification = {}) {
9
+ if (normalizeText(notification.method) !== "thread/tokenUsage/updated") {
10
+ return null;
11
+ }
12
+ const params = codexAppServerNotificationParams(notification);
13
+ const tokenUsage = isPlainObject(params.tokenUsage) ? params.tokenUsage : {};
14
+ const turnUsage = isPlainObject(tokenUsage.last) ? tokenUsage.last : tokenUsage;
15
+ const snapshot = {};
16
+ for (const field of [
17
+ "cachedInputTokens",
18
+ "cacheWriteInputTokens",
19
+ "inputTokens",
20
+ "outputTokens",
21
+ "reasoningOutputTokens",
22
+ "totalTokens"
23
+ ]) {
24
+ const value = Number(turnUsage[field]);
25
+ if (Number.isSafeInteger(value) && value >= 0) {
26
+ snapshot[field] = value;
27
+ }
28
+ }
29
+ return Object.keys(snapshot).length ? Object.freeze(snapshot) : null;
30
+ }
31
+
32
+ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "", {
33
+ includeThreadHistory = true,
34
+ allowEmptyResponse = false,
35
+ onEvent = null,
36
+ timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
37
+ } = {}) {
38
+ const normalizedThreadId = normalizeText(threadId);
39
+ let targetTurnId = "";
40
+ let finalText = "";
41
+ let usage = null;
42
+ let failureDetailTimeout = null;
43
+ let settled = false;
44
+ let timeout = null;
45
+ let connectionCheck = null;
46
+ let unsubscribe = null;
47
+ let pendingCompletionStatus = "";
48
+ let pendingFailure = null;
49
+ let resolveWaiter = null;
50
+ let rejectWaiter = null;
51
+
52
+ function cleanup() {
53
+ clearTimeout(timeout);
54
+ timeout = null;
55
+ clearInterval(connectionCheck);
56
+ connectionCheck = null;
57
+ clearTimeout(failureDetailTimeout);
58
+ failureDetailTimeout = null;
59
+ unsubscribe?.();
60
+ unsubscribe = null;
61
+ }
62
+
63
+ function finish(result = {}) {
64
+ if (settled) {
65
+ return;
66
+ }
67
+ settled = true;
68
+ cleanup();
69
+ resolveWaiter?.(result);
70
+ }
71
+
72
+ function fail(error) {
73
+ if (settled) {
74
+ return;
75
+ }
76
+ settled = true;
77
+ cleanup();
78
+ rejectWaiter?.(error);
79
+ }
80
+
81
+ function emitWatcherEvent(classification = {}, notification) {
82
+ if (typeof onEvent !== "function" || !classification?.kind) {
83
+ return;
84
+ }
85
+ onEvent({
86
+ ...classification,
87
+ threadId: classification.threadId || normalizedThreadId,
88
+ turnId: classification.turnId || targetTurnId
89
+ }, notification);
90
+ }
91
+
92
+ async function resultFromThread() {
93
+ if (
94
+ !includeThreadHistory ||
95
+ !normalizedThreadId ||
96
+ !targetTurnId ||
97
+ typeof provider?.readThread !== "function"
98
+ ) {
99
+ return {
100
+ status: "",
101
+ statusType: "",
102
+ text: ""
103
+ };
104
+ }
105
+ const thread = await provider.readThread(normalizedThreadId);
106
+ const rawStatus = thread.raw?.status || thread.response?.thread?.status;
107
+ return {
108
+ status: codexAppServerStatusFromValue(rawStatus),
109
+ statusType: normalizeText(typeof rawStatus === "string" ? rawStatus : rawStatus?.type),
110
+ text: codexAppServerProviderThreadAssistantSegments(thread, targetTurnId)
111
+ .map((segment) => segment.text)
112
+ .join("\n\n")
113
+ };
114
+ }
115
+
116
+ function failAfterDetailGrace(error) {
117
+ if (settled || failureDetailTimeout) {
118
+ return;
119
+ }
120
+ failureDetailTimeout = setTimeout(() => {
121
+ failureDetailTimeout = null;
122
+ fail(error);
123
+ }, CODEX_APP_SERVER_DETACHED_FAILURE_DETAIL_GRACE_MS);
124
+ }
125
+
126
+ async function finishFromCompletion(status = "completed") {
127
+ try {
128
+ if (!targetTurnId) {
129
+ pendingCompletionStatus = normalizeText(status) || "completed";
130
+ return;
131
+ }
132
+ const authoritative = await resultFromThread().catch(() => ({
133
+ status: "",
134
+ statusType: "",
135
+ text: ""
136
+ }));
137
+ finalText = authoritative.text || finalText;
138
+ if (!finalText && !allowEmptyResponse) {
139
+ pendingCompletionStatus = normalizeText(status) || "completed";
140
+ const systemError = authoritative.statusType === "systemError" || authoritative.status === "failed";
141
+ failAfterDetailGrace(new Error(systemError
142
+ ? "Codex app-server thread entered a system error before producing an assistant response."
143
+ : "Codex app-server completed without producing an assistant response."));
144
+ return;
145
+ }
146
+ finish({
147
+ status,
148
+ text: finalText,
149
+ threadId: normalizedThreadId,
150
+ turnId: targetTurnId,
151
+ usage
152
+ });
153
+ } catch (error) {
154
+ fail(error);
155
+ }
156
+ }
157
+
158
+ function notificationMatches(notification = {}) {
159
+ const notificationThreadId = codexAppServerNotificationThreadId(notification);
160
+ if (notificationThreadId && notificationThreadId !== normalizedThreadId) {
161
+ return false;
162
+ }
163
+ const notificationTurnId = codexAppServerNotificationTurnId(notification);
164
+ return !targetTurnId || !notificationTurnId || notificationTurnId === targetTurnId;
165
+ }
166
+
167
+ return {
168
+ async completeNow(status = "completed") {
169
+ await finishFromCompletion(status);
170
+ },
171
+ failNow(error) {
172
+ fail(error);
173
+ },
174
+ failAfterDetailGrace(error) {
175
+ failAfterDetailGrace(error);
176
+ },
177
+ setTurnId(turnId = "") {
178
+ targetTurnId = normalizeText(turnId);
179
+ if (pendingFailure) {
180
+ failAfterDetailGrace(pendingFailure);
181
+ return;
182
+ }
183
+ if (pendingCompletionStatus) {
184
+ const status = pendingCompletionStatus;
185
+ pendingCompletionStatus = "";
186
+ void finishFromCompletion(status);
187
+ }
188
+ },
189
+ wait() {
190
+ if (settled) {
191
+ return Promise.reject(new Error("Codex app-server detached turn watcher was already settled."));
192
+ }
193
+ return new Promise((resolve, reject) => {
194
+ resolveWaiter = resolve;
195
+ rejectWaiter = reject;
196
+ if (timeoutMs > 0) {
197
+ timeout = setTimeout(() => {
198
+ fail(new Error("Timed out waiting for Codex app-server response."));
199
+ }, timeoutMs);
200
+ } else {
201
+ // Interactive repair turns may take longer than bounded helper jobs.
202
+ // Retain their watcher until completion, interruption, or connection loss.
203
+ const generation = provider?.currentConnectionGeneration?.();
204
+ connectionCheck = setInterval(() => {
205
+ if (provider?.isAvailable?.() === false ||
206
+ provider?.currentConnectionGeneration?.() !== generation) {
207
+ fail(new Error("Connection to Codex was lost while the temporary AI was working."));
208
+ }
209
+ }, 1000);
210
+ connectionCheck.unref?.();
211
+ }
212
+ unsubscribe = typeof provider?.subscribe === "function"
213
+ ? provider.subscribe((notification = {}) => {
214
+ if (!notificationMatches(notification)) {
215
+ return;
216
+ }
217
+ usage = codexAppServerTurnTokenUsage(notification) || usage;
218
+ const classification = classifyCodexAppServerEvent(notification);
219
+ emitWatcherEvent(classification, notification);
220
+ if (
221
+ classification.kind === "provider_error" &&
222
+ classification.text &&
223
+ codexAppServerNotificationParams(notification).willRetry !== true
224
+ ) {
225
+ const error = new Error(classification.text);
226
+ if (!targetTurnId) {
227
+ pendingFailure = error;
228
+ return;
229
+ }
230
+ fail(error);
231
+ return;
232
+ }
233
+ if (classification.kind === "final_assistant_result" && classification.text) {
234
+ finalText = classification.text;
235
+ if (pendingCompletionStatus && targetTurnId) {
236
+ const status = pendingCompletionStatus;
237
+ pendingCompletionStatus = "";
238
+ clearTimeout(failureDetailTimeout);
239
+ failureDetailTimeout = null;
240
+ void finishFromCompletion(status);
241
+ }
242
+ }
243
+ const method = normalizeText(notification.method);
244
+ if (method !== "turn/completed" && method !== "thread/status/changed") {
245
+ return;
246
+ }
247
+ const status = codexAppServerNotificationTurnStatus(notification) || "completed";
248
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
249
+ const error = new Error(codexAppServerNotificationError(notification) || `Codex app-server turn ${status}.`);
250
+ if (!targetTurnId) {
251
+ pendingFailure = error;
252
+ return;
253
+ }
254
+ failAfterDetailGrace(error);
255
+ return;
256
+ }
257
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
258
+ void finishFromCompletion(status);
259
+ }
260
+ })
261
+ : null;
262
+ });
263
+ }
264
+ };
265
+ }
266
+
267
+ function codexAppServerTurnError(reason, details = {}, providerMessage = "") {
268
+ const retryable = reason === "failed";
269
+ const error = new Error(providerMessage || (reason === "identity_missing"
270
+ ? "Codex did not return the exact turn identity."
271
+ : `Codex app-server turn ${details.status}.`));
272
+ error.code = `codex_app_server_turn_${reason}`;
273
+ error.details = { ...details, retryable };
274
+ error.retryable = retryable;
275
+ return error;
276
+ }
277
+
278
+ async function waitForCodexAppServerTurn(provider, threadId = "", turn = null, {
279
+ timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
280
+ createError = codexAppServerTurnError
281
+ } = {}) {
282
+ const normalizedThreadId = normalizeText(threadId || "");
283
+ const turnId = codexAppServerProviderTurnId(turn || {});
284
+ const status = codexAppServerProviderTurnStatus(turn || {});
285
+ if (!normalizedThreadId || !turnId) {
286
+ throw createError("identity_missing", { threadId: normalizedThreadId, turnId });
287
+ }
288
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
289
+ throw createError(
290
+ "failed",
291
+ { status, threadId: normalizedThreadId, turnId },
292
+ codexAppServerProviderTurnError(turn || {})
293
+ );
294
+ }
295
+ const existingText = codexAppServerProviderTurnText({
296
+ raw: { turns: [turn] }
297
+ }, turnId);
298
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status) && existingText) {
299
+ return {
300
+ status,
301
+ text: existingText,
302
+ threadId: normalizedThreadId,
303
+ turnId,
304
+ usage: null
305
+ };
306
+ }
307
+ const watcher = createCodexAppServerDetachedTurnWatcher(provider, normalizedThreadId, {
308
+ timeoutMs
309
+ });
310
+ watcher.setTurnId(turnId);
311
+ const pending = watcher.wait();
312
+ void pending.catch(() => null);
313
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
314
+ await watcher.completeNow(status);
315
+ } else if (typeof provider?.readThread === "function") {
316
+ const latestThread = await provider.readThread(normalizedThreadId);
317
+ const latestTurn = codexAppServerProviderTurnForOperation(latestThread, { turnId });
318
+ const latestStatus = codexAppServerProviderTurnStatus(latestTurn || {});
319
+ if (codexAppServerTurnStatusIsProviderFailure(latestStatus)) {
320
+ watcher.failNow(createError(
321
+ "failed",
322
+ { status: latestStatus, threadId: normalizedThreadId, turnId },
323
+ codexAppServerProviderTurnError(latestTurn || {})
324
+ ));
325
+ } else if (codexAppServerTurnStatusIsSuccessfulComplete(latestStatus)) {
326
+ await watcher.completeNow(latestStatus);
327
+ }
328
+ }
329
+ return pending;
330
+ }
331
+
332
+ export { CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS, createCodexAppServerDetachedTurnWatcher, waitForCodexAppServerTurn };
@@ -3,6 +3,61 @@ import {
3
3
  normalizeText
4
4
  } from "./normalize.js";
5
5
 
6
+ const CODEX_TOKEN_USAGE_METHOD = "thread/tokenUsage/updated";
7
+
8
+ function nonNegativeInteger(value) {
9
+ if (value === null || value === undefined || String(value).trim() === "") {
10
+ return null;
11
+ }
12
+ const number = Number(value);
13
+ return Number.isSafeInteger(number) && number >= 0 ? number : null;
14
+ }
15
+
16
+ function codexContextUsageFromNotification(notification = {}) {
17
+ if (normalizeText(notification?.method) !== CODEX_TOKEN_USAGE_METHOD) {
18
+ return null;
19
+ }
20
+ const params = notification?.params && typeof notification.params === "object"
21
+ ? notification.params
22
+ : {};
23
+ const tokenUsage = params.tokenUsage && typeof params.tokenUsage === "object"
24
+ ? params.tokenUsage
25
+ : {};
26
+ const last = tokenUsage.last && typeof tokenUsage.last === "object"
27
+ ? tokenUsage.last
28
+ : {};
29
+ const total = tokenUsage.total && typeof tokenUsage.total === "object"
30
+ ? tokenUsage.total
31
+ : {};
32
+ const usedTokens = nonNegativeInteger(last.totalTokens);
33
+ const inputTokens = nonNegativeInteger(last.inputTokens);
34
+ const cumulativeTokens = nonNegativeInteger(total.totalTokens);
35
+ const windowTokens = nonNegativeInteger(tokenUsage.modelContextWindow);
36
+ const threadId = normalizeText(params.threadId);
37
+ const turnId = normalizeText(params.turnId);
38
+ if (
39
+ !threadId ||
40
+ !turnId ||
41
+ usedTokens === null ||
42
+ inputTokens === null ||
43
+ cumulativeTokens === null ||
44
+ !windowTokens ||
45
+ usedTokens > windowTokens ||
46
+ inputTokens > usedTokens ||
47
+ cumulativeTokens < usedTokens
48
+ ) {
49
+ return null;
50
+ }
51
+ return Object.freeze({
52
+ cumulativeTokens,
53
+ inputTokens,
54
+ threadId,
55
+ turnId,
56
+ usedTokens,
57
+ windowTokens
58
+ });
59
+ }
60
+
6
61
  const CODEX_APP_SERVER_CONTEXT_COMPACTION_SIGNALS = new Set(
7
62
  [
8
63
  "contextcompaction",
@@ -346,6 +401,11 @@ function classifyCodexAppServerEvent(notification = {}) {
346
401
  threadId: codexAppServerNotificationThreadId(notification),
347
402
  turnId: codexAppServerNotificationTurnId(notification)
348
403
  };
404
+ if (itemType === "contextCompaction" && (method === "item/started" || method === "item/completed")) {
405
+ base.phase = method === "item/started" ? "compacting" : "working";
406
+ } else if (method === "error" && codexAppServerNotificationParams(notification).willRetry === true) {
407
+ base.phase = "retrying";
408
+ }
349
409
 
350
410
  if (method === "item/agentMessage/delta") {
351
411
  const { delta } = codexAppServerNotificationParams(notification);
@@ -376,6 +436,7 @@ function classifyCodexAppServerEvent(notification = {}) {
376
436
  return {
377
437
  ...base,
378
438
  kind: "reasoning_summary",
439
+ delta: codexAppServerContentText(codexAppServerNotificationParams(notification).delta),
379
440
  text: normalizeText(codexAppServerContentText(codexAppServerNotificationParams(notification).delta))
380
441
  };
381
442
  }
@@ -462,6 +523,7 @@ function classifyCodexAppServerEvent(notification = {}) {
462
523
  return {
463
524
  ...base,
464
525
  kind: "status",
526
+ ...(codexAppServerNotificationTurnStatus(notification) === "inProgress" ? { phase: "working" } : {}),
465
527
  text: codexAppServerNotificationTurnStatus(notification)
466
528
  };
467
529
  }
@@ -544,7 +606,7 @@ function codexAppServerProviderThread(value = {}) {
544
606
  }
545
607
 
546
608
  function codexAppServerProviderTurnId(turn = {}) {
547
- return normalizeText(turn.id || turn.turnId || turn.turn_id || turn.turn?.id);
609
+ return normalizeText(turn.id || turn.turnId || turn.turn_id || turn.turn?.id || "");
548
610
  }
549
611
 
550
612
  function codexAppServerProviderTurnItems(turn = {}) {
@@ -623,8 +685,139 @@ function codexAppServerOutputOwnerTurnId({
623
685
  : normalizeText(notificationTurnId);
624
686
  }
625
687
 
688
+ function codexAppServerThreadRawValue(thread = {}) {
689
+ if (isPlainObject(thread.raw)) {
690
+ return thread.raw;
691
+ }
692
+ if (isPlainObject(thread.response?.thread)) {
693
+ return thread.response.thread;
694
+ }
695
+ return isPlainObject(thread) ? thread : {};
696
+ }
697
+
698
+ function codexAppServerThreadTurnId(thread = {}) {
699
+ const observedTurnId = normalizeText(thread.observedTurn?.id || "");
700
+ if (observedTurnId) {
701
+ return observedTurnId;
702
+ }
703
+ const rawThread = codexAppServerThreadRawValue(thread);
704
+ const status = isPlainObject(rawThread.status) ? rawThread.status : {};
705
+ return normalizeText(
706
+ thread.turnId ||
707
+ thread.turn_id ||
708
+ thread.turn?.id ||
709
+ rawThread.turnId ||
710
+ rawThread.turn_id ||
711
+ rawThread.turn?.id ||
712
+ rawThread.currentTurnId ||
713
+ rawThread.current_turn_id ||
714
+ rawThread.activeTurnId ||
715
+ rawThread.active_turn_id ||
716
+ status.turnId ||
717
+ status.turn_id ||
718
+ status.turn?.id ||
719
+ status.currentTurnId ||
720
+ status.current_turn_id ||
721
+ status.activeTurnId ||
722
+ status.active_turn_id ||
723
+ ""
724
+ );
725
+ }
726
+
727
+ function codexAppServerThreadError(thread = {}) {
728
+ const rawThread = codexAppServerThreadRawValue(thread);
729
+ const status = isPlainObject(rawThread.status) ? rawThread.status : {};
730
+ return codexAppServerErrorText(rawThread.error || status.error);
731
+ }
732
+
733
+ function codexAppServerProviderThreadTurns(thread = null) {
734
+ const rawThread = codexAppServerThreadRawValue(thread || {});
735
+ return (Array.isArray(rawThread.turns) ? rawThread.turns : [])
736
+ .filter((turn) => isPlainObject(turn));
737
+ }
738
+
739
+ function codexAppServerProviderTurnStatus(turn = {}) {
740
+ return codexAppServerStatusFromValue(turn.status || turn.state);
741
+ }
742
+
743
+ function codexAppServerProviderTurnClientIds(turn = {}) {
744
+ return codexAppServerProviderTurnItems(turn)
745
+ .map((item) => normalizeText(
746
+ item.clientId ||
747
+ item.client_id ||
748
+ item.clientUserMessageId ||
749
+ item.client_user_message_id ||
750
+ ""
751
+ ))
752
+ .filter(Boolean);
753
+ }
754
+
755
+ function codexAppServerProviderTurnForOperation(thread = null, {
756
+ clientMessageId = "",
757
+ turnId = ""
758
+ } = {}) {
759
+ const normalizedTurnId = normalizeText(turnId || "");
760
+ const normalizedClientMessageId = normalizeText(clientMessageId || "");
761
+ const turns = codexAppServerProviderThreadTurns(thread);
762
+ if (normalizedTurnId) {
763
+ return turns.find((turn) => (
764
+ codexAppServerProviderTurnId(turn) === normalizedTurnId
765
+ )) || null;
766
+ }
767
+ if (!normalizedClientMessageId) {
768
+ return null;
769
+ }
770
+ return [...turns].reverse().find((turn) => (
771
+ codexAppServerProviderTurnClientIds(turn).includes(normalizedClientMessageId)
772
+ )) || null;
773
+ }
774
+
775
+ // Native snapshot matching from the original renewal handover/seed paths.
776
+ // The application projects these classifications into its approval/error policy.
777
+ function inspectCodexAppServerRenewalThread(thread = null, {
778
+ clientMessageId = "",
779
+ turnId: expectedTurnId = "",
780
+ requireFresh = false
781
+ } = {}) {
782
+ const snapshotTurns = codexAppServerProviderThreadTurns(thread);
783
+ const targetTurn = codexAppServerProviderTurnForOperation(thread, {
784
+ clientMessageId,
785
+ turnId: expectedTurnId
786
+ });
787
+ if (expectedTurnId && !targetTurn) return { targetTurn, failure: "turn_missing" };
788
+ if (!requireFresh && !targetTurn && snapshotTurns.length === 0) return { targetTurn, failure: "history_missing" };
789
+ if (requireFresh && snapshotTurns.some((turn) => (
790
+ !targetTurn || codexAppServerProviderTurnId(turn) !== codexAppServerProviderTurnId(targetTurn)
791
+ ))) return { targetTurn, failure: "unrelated_history" };
792
+ return { targetTurn, failure: "" };
793
+ }
794
+
795
+ function codexAppServerProviderTurnText(thread = null, turnId = "") {
796
+ return codexAppServerProviderThreadAssistantSegments(thread || {}, turnId)
797
+ .map((segment) => segment.text)
798
+ .join("\n\n")
799
+ .trim();
800
+ }
801
+
802
+ function codexAppServerProviderTurnError(turn = {}) {
803
+ return codexAppServerErrorText(turn.error || turn.status?.error || turn.state?.error);
804
+ }
805
+
626
806
  export {
807
+ codexContextUsageFromNotification,
627
808
  classifyCodexAppServerEvent,
809
+ codexAppServerThreadRawValue,
810
+ codexAppServerThreadTurnId,
811
+ codexAppServerThreadError,
812
+ codexAppServerProviderThreadTurns,
813
+ codexAppServerProviderTurnId,
814
+ codexAppServerProviderTurnStatus,
815
+ codexAppServerProviderTurnItems,
816
+ codexAppServerProviderTurnClientIds,
817
+ codexAppServerProviderTurnForOperation,
818
+ inspectCodexAppServerRenewalThread,
819
+ codexAppServerProviderTurnText,
820
+ codexAppServerProviderTurnError,
628
821
  codexAppServerAssistantItemText,
629
822
  codexAppServerContentText,
630
823
  codexAppServerContextRefreshReason,
@@ -0,0 +1,124 @@
1
+ import { CODEX_APP_SERVER_AGENT_RUN_ID } from "./codexTurnState.js";
2
+
3
+ // Private composition of unchanged operations from the original run owner.
4
+ export function createCodexGoalCommands({
5
+ conversationContext,
6
+ ensureThreadReady,
7
+ publish,
8
+ readCodexAppServerAgentRunForSession,
9
+ reconcileCodexAppServerGoalUpdated,
10
+ submitCodexAppServerAssistantResult,
11
+ subscribeCodexAppServerEvents,
12
+ turnState
13
+ }) {
14
+ async function prepareCodexAppServerGoalContext(sessionId, options, preparation, { prepareThread = false } = {}) {
15
+ let context;
16
+ if (prepareThread) {
17
+ const prepared = await preparation.readiness(ensureThreadReady);
18
+ if (prepared.ok === false) return prepared;
19
+ context = await conversationContext(sessionId, {}, {});
20
+ } else {
21
+ // This is the current command's authorized context. Goal controls
22
+ // may deliberately retain a pinned selection after a model switch.
23
+ context = await conversationContext(sessionId, {}, options === undefined ? {} : options);
24
+ }
25
+ return preparation.project(context, options);
26
+ }
27
+
28
+ function validateCodexAppServerGoalInput(input = {}) {
29
+ if (!["set", "pause", "resume", "cancel"].includes(input.action)) {
30
+ return { ok: false, error: "Choose set, pause, resume, or cancel." };
31
+ }
32
+ if (input.action === "set" && (typeof input.objective !== "string" || !input.objective.trim() ||
33
+ (input.tokenBudget !== undefined && (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget <= 0)))) {
34
+ return { ok: false, error: "Enter a goal objective and an optional positive whole-number token budget." };
35
+ }
36
+ return null;
37
+ }
38
+
39
+ async function updateCodexAppServerGoal(sessionId, input = {}, {
40
+ context: initialContext,
41
+ prepareConversation
42
+ } = {}) {
43
+ let context = initialContext;
44
+ if (context.ok === false) return context;
45
+ let threadId = context.threadId;
46
+ if (threadId !== input.threadId || (!threadId && input.action !== "set")) {
47
+ return { ok: false, error: "The Codex conversation changed. Refresh the goal before trying again." };
48
+ }
49
+ if (input.action === "set") {
50
+ if (turnState(context.session).status === "observation_lost") {
51
+ return { ok: false, error: "Resume or send a message to restore the stopped conversation before setting a goal." };
52
+ }
53
+ if (!threadId) {
54
+ context = await prepareConversation();
55
+ if (context.ok === false) return context;
56
+ threadId = context.threadId;
57
+ }
58
+ const { goal: previousGoal } = await context.provider.readGoal(threadId);
59
+ if (previousGoal && (previousGoal.status !== "complete" || previousGoal.createdAt !== input.createdAt)) {
60
+ return { ok: false, error: "The Codex goal changed. Refresh it before setting a new goal." };
61
+ }
62
+ subscribeCodexAppServerEvents(sessionId, context.provider, threadId, context);
63
+ const result = await context.provider.setGoal(threadId, {
64
+ objective: input.objective, tokenBudget: input.tokenBudget
65
+ });
66
+ await reconcileCodexAppServerGoalUpdated(sessionId, context.provider, threadId, {
67
+ params: { goal: result.goal }
68
+ });
69
+ await publish(sessionId, { reason: "codex-goal", nativeGoal: { threadId, goal: result.goal || null } });
70
+ return { ok: true, status: "available", threadId, goal: result.goal || null };
71
+ }
72
+ const { goal } = await context.provider.readGoal(threadId);
73
+ if (!goal || goal.status === "complete" || goal.createdAt !== input.createdAt || goal.objective !== input.objective) {
74
+ return { ok: false, error: "The Codex goal changed. Refresh it before trying again." };
75
+ }
76
+ if (input.action === "cancel") {
77
+ subscribeCodexAppServerEvents(sessionId, context.provider, threadId, context);
78
+ await context.provider.clearGoal(threadId);
79
+ await reconcileCodexAppServerGoalUpdated(sessionId, context.provider, threadId, {
80
+ method: "thread/goal/cleared",
81
+ params: { threadId }
82
+ });
83
+ await publish(sessionId, { reason: "codex-goal", nativeGoal: { threadId, goal: null } });
84
+ return { ok: true, status: "available", threadId, goal: null };
85
+ }
86
+ if (input.action === "pause" && !["active", "paused"].includes(goal.status)) {
87
+ return { ok: false, error: "This goal is already stopped. Refresh its details." };
88
+ }
89
+ const observationLost = turnState(context.session).status === "observation_lost";
90
+ if (observationLost && turnState(context.session).active) {
91
+ return { ok: false, error: "Codex has not yet confirmed a stop. Retry after the service has stopped." };
92
+ }
93
+ if (input.action === "resume" && !["paused", "blocked", "usageLimited", ...(observationLost ? ["active"] : [])].includes(goal.status)) {
94
+ return { ok: false, error: "This goal cannot be resumed from its current status. Refresh its details." };
95
+ }
96
+ subscribeCodexAppServerEvents(sessionId, context.provider, threadId, context);
97
+ if (observationLost && input.action === "resume") {
98
+ const recovered = await submitCodexAppServerAssistantResult(sessionId, threadId, turnState(context.session).turnId, { recoverFromProvider: true });
99
+ if (recovered.reason === "error") throw new Error(recovered.error);
100
+ await context.runtime.store.mutateSession(sessionId, async () => {
101
+ const run = await readCodexAppServerAgentRunForSession(context.runtime.store, sessionId);
102
+ if (run.active || run.providerThreadId !== threadId) throw new Error("The assistant changed. Refresh before resuming.");
103
+ await context.runtime.store.writeAgentRunEvent(sessionId, CODEX_APP_SERVER_AGENT_RUN_ID, {
104
+ event: { kind: "codex-observation-explicit-resume" },
105
+ patch: { providerStatus: "interrupted", error: "" }
106
+ });
107
+ });
108
+ await context.provider.resumeThread(threadId, context.resumeOptions);
109
+ }
110
+ const result = await context.provider.setGoalStatus(threadId, input.action === "pause" ? "paused" : "active");
111
+ await reconcileCodexAppServerGoalUpdated(sessionId, context.provider, threadId, {
112
+ params: { goal: result.goal }
113
+ });
114
+ await publish(sessionId, { reason: "codex-goal", nativeGoal: { threadId, goal: result.goal || null } });
115
+ return { ok: true, status: "available", goal: result.goal || null };
116
+ }
117
+
118
+
119
+ return {
120
+ prepareCodexAppServerGoalContext,
121
+ validateCodexAppServerGoalInput,
122
+ updateCodexAppServerGoal
123
+ };
124
+ }