@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.
- package/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- 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
|
+
}
|