@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,482 @@
1
+ import {
2
+ codexAppServerTurnStatusIsActive,
3
+ codexAppServerTurnStatusIsSuccessfulComplete,
4
+ codexAppServerTurnStatusIsProviderFailure,
5
+ normalizeCodexRunText,
6
+ CODEX_APP_SERVER_AGENT_RUN_ID,
7
+ CODEX_AGENT_PROVIDER,
8
+ CODEX_APP_SERVER_RUN_STATE
9
+ } from "./codexTurnState.js";
10
+ import { codexAppServerProviderConnectionGeneration } from "./codexProvider.js";
11
+ import {
12
+ codexAppServerAssistantItemText,
13
+ codexAppServerNotificationEvent,
14
+ codexAppServerNotificationEventType,
15
+ codexAppServerNotificationEventPayload,
16
+ codexAppServerNotificationItem,
17
+ classifyCodexAppServerEvent,
18
+ codexAppServerNotificationParams,
19
+ codexAppServerNotificationThreadId,
20
+ codexAppServerNotificationTurnId,
21
+ codexAppServerNotificationTurnStatus,
22
+ codexAppServerNotificationError,
23
+ codexAppServerContextRefreshReason,
24
+ codexAppServerNotificationUsageLimitExceeded
25
+ } from "./codexEvents.js";
26
+
27
+ // Original native notifications, goal updates and provider-generation subscriptions.
28
+ // The existing queue and operation owners retain their state and ordering.
29
+ export function createCodexNotificationObserver({
30
+ journal,
31
+ recovery,
32
+ threadStatus,
33
+ output,
34
+ delivery,
35
+ notificationQueue,
36
+ namespace,
37
+ createRuntime,
38
+ createStore,
39
+ publish,
40
+ captureContext,
41
+ debugLog,
42
+ onNotificationSignal,
43
+ turnStateFromAgentRun
44
+ }) {
45
+ const { resolveTurnId: resolveCodexAppServerTurnId } = journal;
46
+ const {
47
+ recoverFinalizingTurn: recoverCodexAppServerFinalizingTurn,
48
+ markTurnActive: markCodexAppServerTurnActive,
49
+ markProviderTurnActive: markCodexAppServerProviderTurnActive,
50
+ stopTurnWithProviderFailure: stopCodexAppServerTurnWithProviderFailure,
51
+ completeTurn: completeCodexAppServerTurn
52
+ } = recovery;
53
+ const { reconcileThreadStatus: reconcileCodexAppServerThreadStatus } = threadStatus;
54
+ const {
55
+ readAgentRunForSession: readCodexAppServerAgentRunForSession,
56
+ recordReasoningForSession: recordCodexAppServerReasoningForSession,
57
+ writeStream: writeCodexAppServerStream,
58
+ recordFinalAssistantResult: recordCodexAppServerFinalAssistantResult,
59
+ writeLiveProgress: writeCodexAppServerLiveProgress,
60
+ mirrorTerminalAssistantMessage: mirrorCodexAppServerTerminalAssistantMessage
61
+ } = output;
62
+ const { mirrorTerminalUserMessage: mirrorCodexAppServerTerminalUserMessage } = delivery;
63
+ const codexAppServerNotificationQueue = notificationQueue;
64
+ const { run: runCodexAppServerNotificationTask } = codexAppServerNotificationQueue;
65
+ const CODEX_APP_SERVER_GOAL_STATUSES = new Set([
66
+ "active", "blocked", "budgetLimited", "complete", "paused", "usageLimited"
67
+ ]);
68
+ const codexAppServerEventSubscriptions = new Map();
69
+ const codexAppServerAutomaticHookThreads = new Set();
70
+
71
+ function codexAppServerEventSubscriptionKey(providerKey = "", threadId = "") {
72
+ return `${normalizeCodexRunText(providerKey)}:${normalizeCodexRunText(threadId)}`;
73
+ }
74
+
75
+ function codexAppServerEventSubscriptionIsCurrent(key = "", provider = null) {
76
+ const record = codexAppServerEventSubscriptions.get(key);
77
+ if (!record) {
78
+ return false;
79
+ }
80
+ const providerGeneration = codexAppServerProviderConnectionGeneration(provider);
81
+ return !providerGeneration || record.connectionGeneration === providerGeneration;
82
+ }
83
+
84
+ function unsubscribeCodexAppServerEventSubscription(key = "") {
85
+ const record = codexAppServerEventSubscriptions.get(key);
86
+ record?.unsubscribe?.();
87
+ codexAppServerEventSubscriptions.delete(key);
88
+ }
89
+
90
+ function queueCodexAppServerReasoning(context, notification) {
91
+ return codexAppServerNotificationQueue.reasoning(context, notification, recordCodexAppServerReasoningForSession);
92
+ }
93
+
94
+ function queueCodexAppServerStream(context, classification) {
95
+ return codexAppServerNotificationQueue.stream(context, classification, writeCodexAppServerStream);
96
+ }
97
+
98
+ async function reconcileCodexAppServerGoalUpdated(
99
+ sessionId = "",
100
+ provider = null,
101
+ threadId = "",
102
+ notification = {}
103
+ ) {
104
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
105
+ const normalizedThreadId = normalizeCodexRunText(threadId);
106
+ const notificationTurnId = codexAppServerNotificationTurnId(notification);
107
+ const goalStatus = normalizeCodexRunText(notification.params?.goal?.status);
108
+ const goalCleared = notification.method === "thread/goal/cleared";
109
+ const runtime = await createRuntime();
110
+ let run = await readCodexAppServerAgentRunForSession(
111
+ runtime.store,
112
+ normalizedSessionId
113
+ );
114
+ if (run?.providerStatus === "observation_lost") return { ok: true, processed: false, reason: "observation_lost" };
115
+ const currentThreadId = normalizeCodexRunText(run?.providerThreadId);
116
+ if (currentThreadId && currentThreadId !== normalizedThreadId) {
117
+ return { ok: true, processed: false, reason: "goal_thread_changed" };
118
+ }
119
+ if (
120
+ (goalCleared || CODEX_APP_SERVER_GOAL_STATUSES.has(goalStatus)) &&
121
+ (
122
+ normalizeCodexRunText(run?.providerGoalStatus) !== goalStatus ||
123
+ normalizeCodexRunText(run?.providerGoalThreadId) !== normalizedThreadId
124
+ )
125
+ ) {
126
+ const updatedAt = new Date().toISOString();
127
+ await runtime.store.writeAgentRunEvent(normalizedSessionId, CODEX_APP_SERVER_AGENT_RUN_ID, {
128
+ event: {
129
+ goalStatus,
130
+ kind: "codex-app-server-goal-status-updated",
131
+ message: "",
132
+ providerThreadId: normalizedThreadId
133
+ },
134
+ patch: {
135
+ ...(!run ? {
136
+ provider: CODEX_AGENT_PROVIDER,
137
+ providerInterface: "codex_app_server",
138
+ providerThreadId: normalizedThreadId,
139
+ state: CODEX_APP_SERVER_RUN_STATE.COMPLETED
140
+ } : {}),
141
+ providerGoalStatus: goalStatus,
142
+ providerGoalThreadId: normalizedThreadId,
143
+ providerGoalUpdatedAt: updatedAt
144
+ }
145
+ });
146
+ run = await readCodexAppServerAgentRunForSession(
147
+ runtime.store,
148
+ normalizedSessionId
149
+ );
150
+ }
151
+ const turn = turnStateFromAgentRun(run || {});
152
+ const alreadyFollowingGoalTurn = (
153
+ goalStatus === "active" &&
154
+ turn.active &&
155
+ ["active", "finalizing"].includes(turn.state) &&
156
+ turn.threadId === normalizedThreadId &&
157
+ (!notificationTurnId || turn.turnId === notificationTurnId)
158
+ );
159
+ if (alreadyFollowingGoalTurn) {
160
+ return {
161
+ ok: true,
162
+ processed: false,
163
+ reason: "goal_turn_already_active"
164
+ };
165
+ }
166
+ if (
167
+ (goalCleared || CODEX_APP_SERVER_GOAL_STATUSES.has(goalStatus)) &&
168
+ goalStatus !== "active" &&
169
+ turn.state === "finalizing" &&
170
+ turn.threadId === normalizedThreadId &&
171
+ (!notificationTurnId || turn.turnId === notificationTurnId)
172
+ ) {
173
+ return recoverCodexAppServerFinalizingTurn(
174
+ normalizedSessionId,
175
+ normalizedThreadId,
176
+ turn.turnId,
177
+ {
178
+ status: turn.status || "completed"
179
+ }
180
+ );
181
+ }
182
+ return reconcileCodexAppServerThreadStatus(
183
+ normalizedSessionId,
184
+ provider,
185
+ normalizedThreadId,
186
+ {
187
+ observeLatestTurn: true,
188
+ source: "goal_updated"
189
+ }
190
+ );
191
+ }
192
+
193
+ function subscribeCodexAppServerEvents(sessionId = "", provider = null, threadId = "", options = {}) {
194
+ const normalizedSessionId = normalizeCodexRunText(sessionId);
195
+ const normalizedThreadId = normalizeCodexRunText(threadId);
196
+ if (!normalizedSessionId || !normalizedThreadId || typeof provider?.subscribe !== "function") {
197
+ throw new Error("Codex thread observation requires a session, thread and provider subscription.");
198
+ }
199
+ const projectContext = captureContext();
200
+ const sessionKey = namespace(normalizedSessionId);
201
+ const providerKey = normalizeCodexRunText(options.providerKey);
202
+ const key = codexAppServerEventSubscriptionKey(providerKey, normalizedThreadId);
203
+ const existing = codexAppServerEventSubscriptions.get(key);
204
+ if (existing && codexAppServerEventSubscriptionIsCurrent(key, provider)) {
205
+ return {
206
+ ok: true,
207
+ status: "alreadySubscribed"
208
+ };
209
+ }
210
+ if (existing) {
211
+ unsubscribeCodexAppServerEventSubscription(key);
212
+ }
213
+ // A new observer cannot confirm a previously active phase. Fresh native
214
+ // events restore it; a stored phase alone must not animate activity.
215
+ runCodexAppServerNotificationTask({ projectContext, provider, sessionId: normalizedSessionId, sessionKey }, async () => {
216
+ const store = await createStore(normalizedSessionId);
217
+ const run = await readCodexAppServerAgentRunForSession(store, normalizedSessionId);
218
+ if (!run?.providerPhase || run.providerThreadId !== normalizedThreadId) return;
219
+ await markCodexAppServerTurnActive(normalizedSessionId, {
220
+ phase: "", requireTrackedTurn: true,
221
+ threadId: normalizedThreadId, turnId: run.providerTurnId
222
+ });
223
+ });
224
+ const onNotification = (notification = {}) => {
225
+ const method = normalizeCodexRunText(notification.method);
226
+ if (method === "account/rateLimits/updated" || method === "account/updated") {
227
+ runCodexAppServerNotificationTask({ method, projectContext, provider, sessionId: normalizedSessionId, sessionKey }, () =>
228
+ publish(normalizedSessionId, { reason: "codex-plan-usage" }));
229
+ return;
230
+ }
231
+ const notificationThreadId = codexAppServerNotificationThreadId(notification);
232
+ if (notificationThreadId !== normalizedThreadId) {
233
+ return;
234
+ }
235
+ const notificationContext = {
236
+ method,
237
+ projectContext,
238
+ provider,
239
+ sessionId: normalizedSessionId,
240
+ sessionKey,
241
+ threadId: normalizedThreadId,
242
+ turnId: codexAppServerNotificationTurnId(notification)
243
+ };
244
+ if (method === "thread/tokenUsage/updated") {
245
+ runCodexAppServerNotificationTask(notificationContext, async () => {
246
+ const store = await createStore(normalizedSessionId);
247
+ return onNotificationSignal("usage", {
248
+ store, sessionId: normalizedSessionId, threadId: normalizedThreadId, notification
249
+ });
250
+ });
251
+ }
252
+ const classification = classifyCodexAppServerEvent(notification);
253
+ if (classification.kind === "hook_prompt") {
254
+ codexAppServerAutomaticHookThreads.add(normalizedThreadId);
255
+ return;
256
+ }
257
+ const contextRefreshReason = codexAppServerContextRefreshReason(notification);
258
+ const itemType = codexAppServerNotificationItem(notification)?.type;
259
+ const retrying = classification.phase === "retrying";
260
+ if (!codexAppServerAutomaticHookThreads.has(normalizedThreadId) &&
261
+ (retrying || method === "item/started" || method === "item/completed" && ["contextCompaction", "reasoning"].includes(itemType))) {
262
+ runCodexAppServerNotificationTask(notificationContext, async () => {
263
+ const phase = retrying ? "retrying" : method === "item/started"
264
+ ? ({ contextCompaction: "compacting", reasoning: "reasoning" })[itemType] || ""
265
+ : "";
266
+ const store = await createStore(normalizedSessionId);
267
+ const run = await readCodexAppServerAgentRunForSession(store, normalizedSessionId);
268
+ if (normalizeCodexRunText(run?.providerPhase) === phase) return;
269
+ return markCodexAppServerTurnActive(normalizedSessionId, {
270
+ phase, requireTrackedTurn: true,
271
+ threadId: normalizedThreadId,
272
+ turnId: codexAppServerNotificationTurnId(notification)
273
+ });
274
+ });
275
+ }
276
+ if (contextRefreshReason) {
277
+ runCodexAppServerNotificationTask(notificationContext, () => {
278
+ return onNotificationSignal("context_refresh", {
279
+ sessionId: normalizedSessionId, threadId: normalizedThreadId,
280
+ notification, reason: contextRefreshReason
281
+ });
282
+ });
283
+ }
284
+ if (method === "thread/goal/cleared") {
285
+ runCodexAppServerNotificationTask(notificationContext, async () => {
286
+ await reconcileCodexAppServerGoalUpdated(normalizedSessionId, provider, normalizedThreadId, notification);
287
+ await publish(normalizedSessionId, { reason: "codex-goal", nativeGoal: { threadId: normalizedThreadId, goal: null } });
288
+ });
289
+ return;
290
+ }
291
+ if (method === "thread/goal/updated") {
292
+ runCodexAppServerNotificationTask(notificationContext, async () => {
293
+ await publish(normalizedSessionId, { reason: "codex-goal", nativeGoal: { threadId: normalizedThreadId, goal: notification.params.goal } });
294
+ return reconcileCodexAppServerGoalUpdated(
295
+ normalizedSessionId,
296
+ provider,
297
+ normalizedThreadId,
298
+ notification
299
+ );
300
+ });
301
+ return;
302
+ }
303
+ if (
304
+ (method.startsWith("item/reasoning/") ||
305
+ method === "item/completed" && codexAppServerNotificationItem(notification)?.type === "reasoning") &&
306
+ !codexAppServerAutomaticHookThreads.has(normalizedThreadId)
307
+ ) {
308
+ queueCodexAppServerReasoning(notificationContext, notification);
309
+ }
310
+ if (["assistant_started", "assistant_delta"].includes(classification.kind) &&
311
+ !codexAppServerAutomaticHookThreads.has(normalizedThreadId)) {
312
+ queueCodexAppServerStream(notificationContext, classification);
313
+ }
314
+ if (classification.kind === "final_assistant_result") {
315
+ const event = codexAppServerNotificationEvent(notification);
316
+ const payload = codexAppServerNotificationEventPayload(notification, event);
317
+ const params = codexAppServerNotificationParams(notification);
318
+ runCodexAppServerNotificationTask(notificationContext, () => {
319
+ return recordCodexAppServerFinalAssistantResult({
320
+ itemId: classification.itemId,
321
+ notification,
322
+ sessionId: normalizedSessionId,
323
+ source: classification.source,
324
+ text: classification.text,
325
+ threadId: normalizedThreadId,
326
+ turnId: normalizeCodexRunText(
327
+ params.turnId ||
328
+ params.turn_id ||
329
+ params.turn?.id
330
+ )
331
+ });
332
+ });
333
+ debugLog("appServerFinalAssistantResult.received", {
334
+ eventId: normalizeCodexRunText(event?.id),
335
+ eventType: codexAppServerNotificationEventType(notification, event),
336
+ itemId: normalizeCodexRunText(codexAppServerNotificationItem(notification)?.id),
337
+ method,
338
+ payloadId: normalizeCodexRunText(payload?.id),
339
+ stableItemId: classification.itemId,
340
+ source: classification.source,
341
+ sessionId: normalizedSessionId,
342
+ threadId: normalizedThreadId,
343
+ turnId: classification.turnId
344
+ });
345
+ }
346
+ if (
347
+ classification.kind === "thinking" ||
348
+ classification.kind === "live_progress"
349
+ ) {
350
+ if (codexAppServerAutomaticHookThreads.has(normalizedThreadId)) {
351
+ return;
352
+ }
353
+ runCodexAppServerNotificationTask(notificationContext, () => {
354
+ return writeCodexAppServerLiveProgress(normalizedSessionId, normalizedThreadId, notification);
355
+ });
356
+ }
357
+ if (method === "item/completed") {
358
+ const item = codexAppServerNotificationItem(notification);
359
+ if (normalizeCodexRunText(item?.type) === "userMessage") {
360
+ runCodexAppServerNotificationTask(notificationContext, () => {
361
+ return mirrorCodexAppServerTerminalUserMessage(normalizedSessionId, normalizedThreadId, notification);
362
+ });
363
+ return;
364
+ }
365
+ if (codexAppServerAssistantItemText(item)) {
366
+ runCodexAppServerNotificationTask(notificationContext, () => {
367
+ return mirrorCodexAppServerTerminalAssistantMessage(normalizedSessionId, normalizedThreadId, notification);
368
+ });
369
+ return;
370
+ }
371
+ }
372
+ if (method === "turn/started") {
373
+ codexAppServerAutomaticHookThreads.delete(normalizedThreadId);
374
+ runCodexAppServerNotificationTask(notificationContext, () => markCodexAppServerProviderTurnActive(normalizedSessionId, {
375
+ source: "turn_started",
376
+ status: codexAppServerNotificationTurnStatus(notification) || "inProgress",
377
+ threadId: normalizedThreadId,
378
+ turnId: codexAppServerNotificationTurnId(notification)
379
+ }));
380
+ return;
381
+ }
382
+ if (method === "turn/completed") {
383
+ codexAppServerAutomaticHookThreads.delete(normalizedThreadId);
384
+ const turnId = codexAppServerNotificationTurnId(notification);
385
+ const status = codexAppServerNotificationTurnStatus(notification) || "completed";
386
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
387
+ runCodexAppServerNotificationTask(notificationContext, () => {
388
+ return stopCodexAppServerTurnWithProviderFailure(normalizedSessionId, normalizedThreadId, turnId, {
389
+ error: codexAppServerNotificationError(notification),
390
+ provider,
391
+ status,
392
+ usageLimitExceeded: codexAppServerNotificationUsageLimitExceeded(notification)
393
+ });
394
+ });
395
+ return;
396
+ }
397
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
398
+ runCodexAppServerNotificationTask(notificationContext, () => {
399
+ return completeCodexAppServerTurn(normalizedSessionId, normalizedThreadId, turnId, {
400
+ provider,
401
+ status
402
+ });
403
+ });
404
+ }
405
+ return;
406
+ }
407
+ if (method === "thread/status/changed") {
408
+ const status = codexAppServerNotificationTurnStatus(notification);
409
+ if (codexAppServerTurnStatusIsActive(status)) {
410
+ runCodexAppServerNotificationTask(notificationContext, async () => {
411
+ const turnId = await resolveCodexAppServerTurnId(
412
+ normalizedSessionId,
413
+ normalizedThreadId,
414
+ codexAppServerNotificationTurnId(notification)
415
+ );
416
+ await markCodexAppServerProviderTurnActive(normalizedSessionId, {
417
+ source: "thread_status_changed",
418
+ status,
419
+ threadId: normalizedThreadId,
420
+ turnId
421
+ });
422
+ });
423
+ return;
424
+ }
425
+ codexAppServerAutomaticHookThreads.delete(normalizedThreadId);
426
+ const turnId = codexAppServerNotificationTurnId(notification);
427
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
428
+ runCodexAppServerNotificationTask(notificationContext, () => {
429
+ return stopCodexAppServerTurnWithProviderFailure(normalizedSessionId, normalizedThreadId, turnId, {
430
+ error: codexAppServerNotificationError(notification),
431
+ provider,
432
+ status,
433
+ usageLimitExceeded: codexAppServerNotificationUsageLimitExceeded(notification)
434
+ });
435
+ });
436
+ return;
437
+ }
438
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
439
+ runCodexAppServerNotificationTask(notificationContext, async () => {
440
+ const resolvedTurnId = await resolveCodexAppServerTurnId(
441
+ normalizedSessionId,
442
+ normalizedThreadId,
443
+ turnId
444
+ );
445
+ return completeCodexAppServerTurn(normalizedSessionId, normalizedThreadId, resolvedTurnId, {
446
+ provider,
447
+ status
448
+ });
449
+ });
450
+ }
451
+ }
452
+ };
453
+ let unsubscribeNotifications;
454
+ try {
455
+ unsubscribeNotifications = provider.subscribe(onNotification);
456
+ if (typeof unsubscribeNotifications !== "function") {
457
+ throw new Error("Codex observation requires an unsubscribe function.");
458
+ }
459
+ } catch (error) {
460
+ provider.failObservation(error);
461
+ throw error;
462
+ }
463
+ codexAppServerEventSubscriptions.set(key, {
464
+ connectionGeneration: codexAppServerProviderConnectionGeneration(provider),
465
+ unsubscribe: unsubscribeNotifications
466
+ });
467
+ return {
468
+ ok: true,
469
+ status: existing ? "resubscribed" : "subscribed"
470
+ };
471
+ }
472
+
473
+ return {
474
+ eventSubscriptions: codexAppServerEventSubscriptions,
475
+ automaticHookThreads: codexAppServerAutomaticHookThreads,
476
+ eventSubscriptionKey: codexAppServerEventSubscriptionKey,
477
+ eventSubscriptionIsCurrent: codexAppServerEventSubscriptionIsCurrent,
478
+ unsubscribeEventSubscription: unsubscribeCodexAppServerEventSubscription,
479
+ reconcileGoalUpdated: reconcileCodexAppServerGoalUpdated,
480
+ subscribeEvents: subscribeCodexAppServerEvents
481
+ };
482
+ }
@@ -0,0 +1,103 @@
1
+ import { normalizeCodexRunText } from "./codexTurnState.js";
2
+
3
+ // Original per-conversation notification order and queued-fragment batching.
4
+ // The existing native delivery methods remain the sink while their connected
5
+ // observer body moves; running batches are never merged or delayed by a timer.
6
+ export function createCodexAppServerNotificationQueue({
7
+ namespace = normalizeCodexRunText,
8
+ isClosing = () => false,
9
+ runInContext = (_context, operation) => operation(),
10
+ reportError = () => {}
11
+ } = {}) {
12
+ const codexAppServerNotificationTasks = new Map();
13
+ const codexAppServerPendingStreams = new Map();
14
+ const codexAppServerPendingReasoning = new Map();
15
+
16
+ function runCodexAppServerNotificationTask(context = {}, operation = async () => null) {
17
+ if (isClosing()) {
18
+ return;
19
+ }
20
+ const taskSessionId = normalizeCodexRunText(context.sessionId);
21
+ const taskSessionKey = normalizeCodexRunText(context.sessionKey) ||
22
+ namespace(taskSessionId);
23
+ // Other notifications end the pending stream batch so completion, reasoning
24
+ // and turn changes retain their position in the provider's event order.
25
+ codexAppServerPendingStreams.delete(taskSessionKey);
26
+ codexAppServerPendingReasoning.delete(taskSessionKey);
27
+ const previous = codexAppServerNotificationTasks.get(taskSessionKey) || Promise.resolve();
28
+ const task = previous
29
+ .catch(() => null)
30
+ .then(() => runInContext(context.projectContext, operation))
31
+ .catch((error) => {
32
+ context.provider?.failObservation?.(error);
33
+ reportError(error, context);
34
+ });
35
+ codexAppServerNotificationTasks.set(taskSessionKey, task);
36
+ void task.finally(() => {
37
+ if (codexAppServerNotificationTasks.get(taskSessionKey) === task) {
38
+ codexAppServerNotificationTasks.delete(taskSessionKey);
39
+ }
40
+ });
41
+ }
42
+
43
+ async function drainCodexAppServerNotificationTasks(sessionId = "") {
44
+ const taskSessionKey = namespace(sessionId);
45
+ while (true) {
46
+ const task = codexAppServerNotificationTasks.get(taskSessionKey);
47
+ if (!task) {
48
+ return;
49
+ }
50
+ await task;
51
+ }
52
+ }
53
+
54
+ function queueCodexAppServerStream(context, classification, deliver) {
55
+ if (isClosing() || !classification.itemId || !classification.turnId) return;
56
+ const pending = codexAppServerPendingStreams.get(context.sessionKey);
57
+ if (classification.kind === "assistant_delta" && pending &&
58
+ pending.threadId === classification.threadId &&
59
+ pending.turnId === classification.turnId && pending.itemId === classification.itemId) {
60
+ pending.delta += classification.delta;
61
+ return;
62
+ }
63
+ const stream = { ...classification, delta: classification.delta || "" };
64
+ runCodexAppServerNotificationTask(context, () => {
65
+ if (codexAppServerPendingStreams.get(context.sessionKey) === stream) {
66
+ codexAppServerPendingStreams.delete(context.sessionKey);
67
+ }
68
+ return deliver(context.sessionId, stream);
69
+ });
70
+ // Combine only fragments still waiting for delivery. The running write owns
71
+ // an immutable batch; there is no timer or delay before the next write.
72
+ codexAppServerPendingStreams.set(context.sessionKey, stream);
73
+ }
74
+
75
+ function queueCodexAppServerReasoning(context, notification, deliver) {
76
+ if (isClosing()) return;
77
+ const pending = codexAppServerPendingReasoning.get(context.sessionKey);
78
+ if (pending && pending.threadId === context.threadId && pending.turnId === context.turnId) {
79
+ pending.notifications.push(notification);
80
+ return;
81
+ }
82
+ const batch = { threadId: context.threadId, turnId: context.turnId, notifications: [notification] };
83
+ runCodexAppServerNotificationTask(context, () => {
84
+ if (codexAppServerPendingReasoning.get(context.sessionKey) === batch) {
85
+ codexAppServerPendingReasoning.delete(context.sessionKey);
86
+ }
87
+ return deliver(context.sessionId, context.threadId, batch.notifications);
88
+ });
89
+ // Merge only work waiting behind a write. Other notifications close this
90
+ // batch, preserving commentary and turn boundaries without a timer.
91
+ codexAppServerPendingReasoning.set(context.sessionKey, batch);
92
+ }
93
+
94
+ return {
95
+ tasks: codexAppServerNotificationTasks,
96
+ pendingStreams: codexAppServerPendingStreams,
97
+ pendingReasoning: codexAppServerPendingReasoning,
98
+ run: runCodexAppServerNotificationTask,
99
+ drain: drainCodexAppServerNotificationTasks,
100
+ stream: queueCodexAppServerStream,
101
+ reasoning: queueCodexAppServerReasoning
102
+ };
103
+ }