@jskit-ai/assistant-core 0.1.200 → 0.1.202

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,549 @@
1
+ import { normalizeText, isPlainObject } from "./normalize.js";
2
+ import { codexAppServerStatusFromValue, codexAppServerThreadRawValue } from "./codexEvents.js";
3
+
4
+ function codexAppServerTurnStatusIsActive(status = "") {
5
+ return normalizeText(status) === "inProgress";
6
+ }
7
+
8
+ function codexAppServerTurnStatusIsComplete(status = "") {
9
+ return ["completed", "interrupted", "failed"].includes(normalizeText(status));
10
+ }
11
+
12
+ function codexAppServerTurnStatusIsSuccessfulComplete(status = "") {
13
+ return normalizeText(status) === "completed";
14
+ }
15
+
16
+ function codexAppServerTurnStatusIsProviderFailure(status = "") {
17
+ return ["failed", "interrupted"].includes(normalizeText(status));
18
+ }
19
+
20
+ function codexAppServerThreadStatus(thread = {}) {
21
+ const liveStatus = codexAppServerStatusFromValue(
22
+ codexAppServerThreadRawValue(thread).status || thread.status
23
+ );
24
+ // A resumed goal can already be running while turn history still reports
25
+ // interruption. Releasing its chat owner would hide progress and steering.
26
+ if (codexAppServerTurnStatusIsActive(liveStatus)) {
27
+ return liveStatus;
28
+ }
29
+ if (isPlainObject(thread.observedTurn)) {
30
+ const observedStatus = codexAppServerStatusFromValue(thread.observedTurn.status);
31
+ if (observedStatus) {
32
+ return observedStatus;
33
+ }
34
+ }
35
+ return liveStatus;
36
+ }
37
+
38
+ async function codexAppServerReadThreadStatus(provider = null, threadId = "", {
39
+ observeLatestTurn = false
40
+ } = {}) {
41
+ const normalizedThreadId = normalizeText(threadId || "");
42
+ if (!normalizedThreadId) {
43
+ return null;
44
+ }
45
+ if (typeof provider?.readThreadStatus === "function") {
46
+ const thread = await provider.readThreadStatus(normalizedThreadId);
47
+ if (!observeLatestTurn || typeof provider?.listThreadTurns !== "function") {
48
+ return thread;
49
+ }
50
+ const response = await provider.listThreadTurns(normalizedThreadId, {
51
+ itemsView: "summary",
52
+ limit: 1,
53
+ sortDirection: "desc"
54
+ });
55
+ const observedTurn = Array.isArray(response?.data) ? response.data[0] : null;
56
+ if (provider.isControlProbeTurn?.(normalizedThreadId, observedTurn?.id)) return thread;
57
+ return isPlainObject(observedTurn)
58
+ ? {
59
+ ...thread,
60
+ observedTurn
61
+ }
62
+ : thread;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ // Original native run projection and guarded mutation sequence. The caller's
68
+ // existing runtime/store owns persistence and its mutation lock; no second
69
+ // request journal or transcript-derived ownership is introduced here.
70
+ const CODEX_APP_SERVER_AGENT_RUN_ID = "codex_app_server";
71
+ const CODEX_APP_SERVER_RESULT_PROCESSED_EVENT = "codex-app-server-result-processed";
72
+ const CODEX_AGENT_PROVIDER = "codex";
73
+ export const CODEX_APP_SERVER_RUN_STATE = Object.freeze({
74
+ ACTIVE: "active",
75
+ CANCELLED: "cancelled",
76
+ COMPLETED: "completed",
77
+ FAILED: "failed",
78
+ FINALIZING: "finalizing",
79
+ INTERRUPTED: "interrupted",
80
+ STARTING: "starting",
81
+ TIMED_OUT: "timed_out"
82
+ });
83
+ const CODEX_APP_SERVER_RUN_STATES = new Set(Object.values(CODEX_APP_SERVER_RUN_STATE));
84
+ const CODEX_APP_SERVER_TERMINAL_RUN_STATES = new Set([
85
+ CODEX_APP_SERVER_RUN_STATE.CANCELLED,
86
+ CODEX_APP_SERVER_RUN_STATE.COMPLETED,
87
+ CODEX_APP_SERVER_RUN_STATE.FAILED,
88
+ CODEX_APP_SERVER_RUN_STATE.INTERRUPTED,
89
+ CODEX_APP_SERVER_RUN_STATE.TIMED_OUT
90
+ ]);
91
+ const CODEX_APP_SERVER_ACTIVE_RUN_STATES = new Set([
92
+ CODEX_APP_SERVER_RUN_STATE.ACTIVE,
93
+ CODEX_APP_SERVER_RUN_STATE.FINALIZING,
94
+ CODEX_APP_SERVER_RUN_STATE.STARTING
95
+ ]);
96
+
97
+ function normalizeCodexRunText(value) {
98
+ return String(value || "").trim();
99
+ }
100
+
101
+ function codexAppServerRunInputSource(run = {}) {
102
+ return normalizeCodexRunText(run?.inputSource);
103
+ }
104
+
105
+ export function normalizeCodexAppServerRunState(state) {
106
+ const normalizedState = normalizeCodexRunText(state) || CODEX_APP_SERVER_RUN_STATE.STARTING;
107
+ if (!CODEX_APP_SERVER_RUN_STATES.has(normalizedState)) {
108
+ throw Object.assign(new Error(`Invalid Codex app-server run state: ${normalizedState}`), {
109
+ code: "codex_app_server_invalid_run_state"
110
+ });
111
+ }
112
+ return normalizedState;
113
+ }
114
+
115
+ export function codexAppServerRunStateIsActive(state) {
116
+ return CODEX_APP_SERVER_ACTIVE_RUN_STATES.has(normalizeCodexAppServerRunState(state));
117
+ }
118
+
119
+ export function codexAppServerRunStateIsTerminal(state) {
120
+ return CODEX_APP_SERVER_TERMINAL_RUN_STATES.has(normalizeCodexAppServerRunState(state));
121
+ }
122
+
123
+ export function codexAppServerAgentRun(session = {}) {
124
+ const runs = Array.isArray(session.agentRuns) ? session.agentRuns : [];
125
+ return runs.find((run) => normalizeCodexRunText(run?.id) === CODEX_APP_SERVER_AGENT_RUN_ID) || null;
126
+ }
127
+
128
+ export function codexAppServerPendingUserMessageClientIds(run = {}) {
129
+ const ids = run?.pendingUserMessageClientIds;
130
+ return (Array.isArray(ids) ? ids : [])
131
+ .map((id) => normalizeCodexRunText(id))
132
+ .filter(Boolean);
133
+ }
134
+
135
+ export function codexAppServerPendingUserMessageOwnership(run = {}, clientId = "") {
136
+ const pendingClientIds = codexAppServerPendingUserMessageClientIds(run);
137
+ const normalizedClientId = normalizeCodexRunText(clientId);
138
+ const ownedClientId = normalizedClientId
139
+ ? pendingClientIds.find((id) => id === normalizedClientId)
140
+ : pendingClientIds[0];
141
+ if (!ownedClientId) {
142
+ return null;
143
+ }
144
+ return {
145
+ clientId: ownedClientId,
146
+ inputSource: normalizeCodexRunText(run?.inputSource)
147
+ };
148
+ }
149
+
150
+ export function codexAppServerProcessedResultEvent(session = {}, threadId = "", turnId = "") {
151
+ const normalizedThreadId = normalizeCodexRunText(threadId);
152
+ const normalizedTurnId = normalizeCodexRunText(turnId);
153
+ const events = codexAppServerAgentRun(session)?.events;
154
+ return (Array.isArray(events) ? events : []).findLast((event) => (
155
+ normalizeCodexRunText(event?.kind) === CODEX_APP_SERVER_RESULT_PROCESSED_EVENT &&
156
+ normalizeCodexRunText(event?.providerThreadId) === normalizedThreadId &&
157
+ normalizeCodexRunText(event?.providerTurnId) === normalizedTurnId
158
+ )) || null;
159
+ }
160
+
161
+ export function codexAppServerTurnStateFromAgentRun(run = {}, normalizeRunState = normalizeCodexAppServerRunState) {
162
+ const runState = normalizeRunState(run.state);
163
+ const active = codexAppServerRunStateIsActive(runState);
164
+ const state = runState === CODEX_APP_SERVER_RUN_STATE.FINALIZING
165
+ ? "finalizing"
166
+ : runState === CODEX_APP_SERVER_RUN_STATE.STARTING
167
+ ? "starting"
168
+ : active
169
+ ? "active"
170
+ : "idle";
171
+ return {
172
+ active,
173
+ completedAt: normalizeCodexRunText(run.finishedAt),
174
+ error: normalizeCodexRunText(run.error),
175
+ goalStatus: normalizeCodexRunText(run.providerGoalStatus),
176
+ goalThreadId: normalizeCodexRunText(run.providerGoalThreadId),
177
+ inputSource: normalizeCodexRunText(run.inputSource),
178
+ outerTurnId: normalizeCodexRunText(run.outerTurnId),
179
+ phase: active && run.providerStatus !== "observation_lost" ? normalizeCodexRunText(run.providerPhase) : "",
180
+ runId: normalizeCodexRunText(run.id),
181
+ runState,
182
+ startedAt: normalizeCodexRunText(run.startedAt),
183
+ state,
184
+ status: normalizeCodexRunText(run.providerStatus || run.status || runState),
185
+ threadId: normalizeCodexRunText(run.providerThreadId),
186
+ turnId: normalizeCodexRunText(run.providerTurnId),
187
+ updatedAt: normalizeCodexRunText(run.updatedAt)
188
+ };
189
+ }
190
+
191
+ export function codexAppServerTurnState(session = {}, normalizeRunState = normalizeCodexAppServerRunState) {
192
+ const run = codexAppServerAgentRun(session);
193
+ if (run) {
194
+ return codexAppServerTurnStateFromAgentRun(run, normalizeRunState);
195
+ }
196
+ return {
197
+ active: false,
198
+ completedAt: "",
199
+ error: "",
200
+ goalStatus: "",
201
+ goalThreadId: "",
202
+ outerTurnId: "",
203
+ runId: "",
204
+ runState: "",
205
+ startedAt: "",
206
+ state: "idle",
207
+ status: "",
208
+ threadId: "",
209
+ turnId: "",
210
+ updatedAt: ""
211
+ };
212
+ }
213
+
214
+ export function codexAppServerTurnOwnsActiveGoal(turn = {}, threadId = "") {
215
+ const normalizedThreadId = normalizeCodexRunText(threadId);
216
+ const goalThreadId = normalizeCodexRunText(turn.goalThreadId);
217
+ const inputSource = normalizeCodexRunText(turn.inputSource);
218
+ return normalizeCodexRunText(turn.goalStatus) === "active" &&
219
+ Boolean(normalizedThreadId) &&
220
+ goalThreadId === normalizedThreadId &&
221
+ Boolean(normalizeCodexRunText(turn.outerTurnId)) &&
222
+ Boolean(inputSource) &&
223
+ inputSource !== "terminal";
224
+ }
225
+
226
+ export function codexAppServerTurnMatches(turn = {}, threadId = "", turnId = "") {
227
+ const normalizedThreadId = normalizeCodexRunText(threadId);
228
+ const normalizedTurnId = normalizeCodexRunText(turnId);
229
+ const currentThreadId = normalizeCodexRunText(turn.threadId);
230
+ const currentTurnId = normalizeCodexRunText(turn.turnId);
231
+ if (normalizedThreadId && normalizeCodexRunText(turn.threadId) !== normalizedThreadId) {
232
+ return false;
233
+ }
234
+ if (normalizedTurnId && currentTurnId && currentTurnId !== normalizedTurnId) {
235
+ return false;
236
+ }
237
+ if (!normalizedTurnId && currentTurnId) {
238
+ return false;
239
+ }
240
+ if (!normalizedThreadId && currentThreadId) {
241
+ return false;
242
+ }
243
+ return true;
244
+ }
245
+
246
+ export function codexAppServerTurnCanReceiveProviderCompletion(turn = {}, threadId = "", turnId = "") {
247
+ return codexAppServerTurnMatches(turn, threadId, turnId) &&
248
+ ["active", "finalizing"].includes(normalizeCodexRunText(turn.state));
249
+ }
250
+
251
+ export function codexAppServerTurnAwaitsProviderIdentity(turn = {}, threadId = "", turnId = "") {
252
+ const normalizedThreadId = normalizeCodexRunText(threadId);
253
+ const normalizedTurnId = normalizeCodexRunText(turnId);
254
+ const currentThreadId = normalizeCodexRunText(turn.threadId);
255
+ return normalizeCodexRunText(turn.state) === "starting" &&
256
+ Boolean(normalizedTurnId) &&
257
+ !normalizeCodexRunText(turn.turnId) &&
258
+ (!normalizedThreadId || !currentThreadId || normalizedThreadId === currentThreadId);
259
+ }
260
+
261
+ export function codexAppServerTurnCanReceiveProviderActivity(turn = {}, threadId = "", turnId = "") {
262
+ if (turn.status === "observation_lost") return false;
263
+ const normalizedThreadId = normalizeCodexRunText(threadId);
264
+ const normalizedTurnId = normalizeCodexRunText(turnId);
265
+ const currentThreadId = normalizeCodexRunText(turn.threadId);
266
+ const currentTurnId = normalizeCodexRunText(turn.turnId);
267
+ if (!["starting", "active"].includes(normalizeCodexRunText(turn.state))) {
268
+ return false;
269
+ }
270
+ if (normalizedThreadId && currentThreadId && currentThreadId !== normalizedThreadId) {
271
+ return false;
272
+ }
273
+ if (normalizedTurnId && currentTurnId && currentTurnId !== normalizedTurnId) {
274
+ return false;
275
+ }
276
+ if (normalizedThreadId && !currentThreadId) {
277
+ return false;
278
+ }
279
+ return true;
280
+ }
281
+
282
+ export function codexAppServerTurnCanAdoptSuccessor(turn = {}, threadId = "", turnId = "") {
283
+ if (turn.status === "observation_lost") return false;
284
+ const normalizedThreadId = normalizeCodexRunText(threadId);
285
+ const normalizedTurnId = normalizeCodexRunText(turnId);
286
+ const currentThreadId = normalizeCodexRunText(turn.threadId);
287
+ const currentTurnId = normalizeCodexRunText(turn.turnId);
288
+ return turn.active === true &&
289
+ ["active", "finalizing"].includes(normalizeCodexRunText(turn.state)) &&
290
+ Boolean(normalizedThreadId) &&
291
+ normalizedThreadId === currentThreadId &&
292
+ Boolean(normalizedTurnId) &&
293
+ Boolean(currentTurnId) &&
294
+ normalizedTurnId !== currentTurnId;
295
+ }
296
+
297
+ export function codexAppServerTurnKey(threadId = "", turnId = "") {
298
+ return `${normalizeCodexRunText(threadId)}:${normalizeCodexRunText(turnId)}`;
299
+ }
300
+
301
+ export function codexAppServerAgentRunPatch({
302
+ error = "",
303
+ inputSource = "",
304
+ outerTurnId = "",
305
+ phase,
306
+ runState = CODEX_APP_SERVER_RUN_STATE.COMPLETED,
307
+ session = {},
308
+ status = "",
309
+ threadId = "",
310
+ turnId = "",
311
+ updatedAt = ""
312
+ } = {}, normalizeRunState = normalizeCodexAppServerRunState) {
313
+ const normalizedRunState = normalizeRunState(runState);
314
+ const currentRun = codexAppServerAgentRun(session);
315
+ const sameActiveTurn = normalizedRunState === CODEX_APP_SERVER_RUN_STATE.ACTIVE &&
316
+ currentRun?.providerThreadId === normalizeCodexRunText(threadId) &&
317
+ currentRun?.providerTurnId === normalizeCodexRunText(turnId);
318
+ const patch = {
319
+ error: normalizeCodexRunText(error),
320
+ provider: CODEX_AGENT_PROVIDER,
321
+ providerInterface: "codex_app_server",
322
+ providerPhase: normalizedRunState === CODEX_APP_SERVER_RUN_STATE.ACTIVE && status !== "observation_lost"
323
+ ? normalizeCodexRunText(phase === undefined && sameActiveTurn ? currentRun?.providerPhase : phase)
324
+ : "",
325
+ providerStatus: normalizeCodexRunText(status),
326
+ providerThreadId: normalizeCodexRunText(threadId),
327
+ providerTurnId: normalizeCodexRunText(turnId),
328
+ state: normalizedRunState,
329
+ updatedAt: normalizeCodexRunText(updatedAt)
330
+ };
331
+ const normalizedInputSource = normalizeCodexRunText(inputSource);
332
+ if (normalizedInputSource) {
333
+ patch.inputSource = normalizedInputSource;
334
+ }
335
+ // A native terminal turn starts its own chat owner. Reusing the previous
336
+ // owner's id would checkpoint two different turns with one identity.
337
+ const normalizedOuterTurnId = normalizeCodexRunText(outerTurnId) ||
338
+ (normalizedInputSource === "terminal" && normalizedRunState === CODEX_APP_SERVER_RUN_STATE.ACTIVE && !sameActiveTurn
339
+ ? `codex:${normalizeCodexRunText(threadId)}:${normalizeCodexRunText(turnId)}`
340
+ : normalizeCodexRunText(currentRun?.outerTurnId));
341
+ if (normalizedOuterTurnId) {
342
+ patch.outerTurnId = normalizedOuterTurnId;
343
+ }
344
+ if (
345
+ normalizedRunState === CODEX_APP_SERVER_RUN_STATE.STARTING ||
346
+ codexAppServerRunStateIsTerminal(normalizedRunState) ||
347
+ normalizedInputSource === "terminal"
348
+ ) {
349
+ patch.pendingUserMessageClientIds = [];
350
+ }
351
+ if ([CODEX_APP_SERVER_RUN_STATE.ACTIVE, CODEX_APP_SERVER_RUN_STATE.STARTING].includes(normalizedRunState)) {
352
+ patch.startedAt = normalizeCodexRunText(updatedAt);
353
+ }
354
+ if (!codexAppServerRunStateIsActive(normalizedRunState)) {
355
+ patch.finishedAt = normalizeCodexRunText(updatedAt);
356
+ }
357
+ return patch;
358
+ }
359
+
360
+ export function codexAppServerRunIdentityForPatch(session = {}, {
361
+ threadId = "",
362
+ turnId = ""
363
+ } = {}, normalizeRunState = normalizeCodexAppServerRunState) {
364
+ const normalizedThreadId = normalizeCodexRunText(threadId);
365
+ const normalizedTurnId = normalizeCodexRunText(turnId);
366
+ if (normalizedTurnId) {
367
+ return {
368
+ threadId: normalizedThreadId,
369
+ turnId: normalizedTurnId
370
+ };
371
+ }
372
+ const currentTurn = codexAppServerTurnState(session, normalizeRunState);
373
+ const currentThreadId = normalizeCodexRunText(currentTurn.threadId);
374
+ if (
375
+ normalizeCodexRunText(currentTurn.turnId) &&
376
+ ["active", "finalizing"].includes(normalizeCodexRunText(currentTurn.state)) &&
377
+ (!normalizedThreadId || !currentThreadId || normalizedThreadId === currentThreadId)
378
+ ) {
379
+ return {
380
+ threadId: normalizedThreadId || currentThreadId,
381
+ turnId: currentTurn.turnId
382
+ };
383
+ }
384
+ return {
385
+ threadId: normalizedThreadId,
386
+ turnId: normalizedTurnId
387
+ };
388
+ }
389
+
390
+ export function codexAppServerRunPatchIsStaleAfterTerminalState(currentTurn = {}, patch = {}, normalizeRunState = normalizeCodexAppServerRunState) {
391
+ const currentRunState = normalizeRunState(currentTurn.runState);
392
+ const patchRunState = normalizeRunState(patch.state);
393
+ if (!codexAppServerRunStateIsTerminal(currentRunState)) {
394
+ return false;
395
+ }
396
+ if (patchRunState === CODEX_APP_SERVER_RUN_STATE.STARTING) {
397
+ return false;
398
+ }
399
+ const currentThreadId = normalizeCodexRunText(currentTurn.threadId);
400
+ const patchThreadId = normalizeCodexRunText(patch.providerThreadId);
401
+ const currentTurnId = normalizeCodexRunText(currentTurn.turnId);
402
+ const patchTurnId = normalizeCodexRunText(patch.providerTurnId);
403
+ const threadMatches = !patchThreadId || !currentThreadId || patchThreadId === currentThreadId;
404
+ const turnMatches = !patchTurnId || (
405
+ Boolean(currentTurnId) &&
406
+ currentTurnId === patchTurnId
407
+ );
408
+ return threadMatches && turnMatches;
409
+ }
410
+
411
+ function codexAppServerErrorMessage(value, fallback = "Codex could not be prepared.") {
412
+ return normalizeCodexRunText(value?.error || value?.message || value) || fallback;
413
+ }
414
+
415
+ export function codexAppServerSteerFailure(result = {}, steerFailedCode = "codex_turn_steer_failed") {
416
+ if (!isPlainObject(result)) {
417
+ return null;
418
+ }
419
+ if (result.ok !== false) {
420
+ return null;
421
+ }
422
+ return {
423
+ code: normalizeCodexRunText(result.code) || steerFailedCode,
424
+ error: codexAppServerErrorMessage(result, "Codex app-server turn could not be steered."),
425
+ ok: false,
426
+ operationOutcome: normalizeCodexRunText(result.operationOutcome) || "steer_failed",
427
+ refreshRecommended: true,
428
+ retryable: result.retryable === true
429
+ };
430
+ }
431
+
432
+ export function codexAppServerMessageRequiresNewTurn({
433
+ reason = "provider_idle",
434
+ threadId = "",
435
+ turnId = ""
436
+ } = {}) {
437
+ return {
438
+ delivered: false,
439
+ deliveryMode: "new_turn",
440
+ newTurnRequired: true,
441
+ ok: true,
442
+ operationOutcome: "new_turn_required",
443
+ reason: normalizeCodexRunText(reason),
444
+ threadId: normalizeCodexRunText(threadId),
445
+ turnId: normalizeCodexRunText(turnId)
446
+ };
447
+ }
448
+
449
+ function codexAppServerMessageDeferred({
450
+ threadId = "",
451
+ turnId = ""
452
+ } = {}, steerFailedCode = "codex_turn_steer_failed") {
453
+ return {
454
+ code: steerFailedCode,
455
+ delivered: false,
456
+ error: "The active assistant operation cannot accept messages yet.",
457
+ ok: false,
458
+ operationOutcome: "active_turn_not_steerable",
459
+ refreshRecommended: true,
460
+ retryable: true,
461
+ threadId,
462
+ turnId
463
+ };
464
+ }
465
+
466
+ export function codexAppServerInterruptFailure(result = {}, interruptFailedCode = "codex_turn_interrupt_failed") {
467
+ if (!isPlainObject(result)) {
468
+ return null;
469
+ }
470
+ if (result.ok !== false && result.interrupted !== false) {
471
+ return null;
472
+ }
473
+ return {
474
+ code: normalizeCodexRunText(result.code) || interruptFailedCode,
475
+ error: codexAppServerErrorMessage(result, "Codex app-server turn could not be interrupted."),
476
+ ok: false,
477
+ operationOutcome: normalizeCodexRunText(result.operationOutcome) || "interrupt_failed",
478
+ refreshRecommended: true,
479
+ retryable: result.retryable === true
480
+ };
481
+ }
482
+
483
+ export function codexAppServerInterruptUnavailableResponse({
484
+ active = false,
485
+ threadId = "",
486
+ turnId = ""
487
+ } = {}, interruptFailedCode = "codex_turn_interrupt_failed") {
488
+ return {
489
+ active: active === true,
490
+ code: interruptFailedCode,
491
+ error: active
492
+ ? "The active Codex app-server turn is not ready to interrupt yet."
493
+ : "No active Codex app-server turn is available to interrupt.",
494
+ ok: false,
495
+ operationOutcome: "interrupt_unavailable",
496
+ refreshRecommended: true,
497
+ retryable: active === true,
498
+ threadId: normalizeCodexRunText(threadId),
499
+ turnId: normalizeCodexRunText(turnId)
500
+ };
501
+ }
502
+
503
+ export function codexAppServerFrozenTurnInterruptResponse({
504
+ threadId = "",
505
+ turnId = ""
506
+ } = {}) {
507
+ return {
508
+ interrupted: false,
509
+ ok: true,
510
+ operationOutcome: "already_idle",
511
+ status: "interrupted",
512
+ threadId: normalizeCodexRunText(threadId),
513
+ turnId: normalizeCodexRunText(turnId)
514
+ };
515
+ }
516
+
517
+ export function codexAppServerConversationTurnIsActive(status = "") {
518
+ return ["starting", "inProgress"].includes(normalizeCodexRunText(status));
519
+ }
520
+
521
+ export function codexAppServerMessageText(input = {}) {
522
+ if (typeof input === "string") {
523
+ return normalizeCodexRunText(input);
524
+ }
525
+ if (!isPlainObject(input)) {
526
+ return "";
527
+ }
528
+ return normalizeCodexRunText(input.message);
529
+ }
530
+
531
+ export function codexAppServerMessageDisplayText(input = {}, fallback = "") {
532
+ if (!isPlainObject(input)) {
533
+ return normalizeCodexRunText(fallback || input);
534
+ }
535
+ return normalizeCodexRunText(input.displayMessage || input.message || fallback);
536
+ }
537
+
538
+ function codexPromptInputFromRequest(input = {}) {
539
+ if (typeof input === "string") {
540
+ return normalizeCodexRunText(input);
541
+ }
542
+ const terminalInput = normalizeCodexRunText(input.terminalInput);
543
+ if (terminalInput) {
544
+ return terminalInput;
545
+ }
546
+ return codexAppServerMessageText(input);
547
+ }
548
+
549
+ export { codexAppServerRunInputSource, codexAppServerThreadStatus, codexAppServerReadThreadStatus, codexAppServerTurnStatusIsActive, codexAppServerTurnStatusIsComplete, codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, normalizeCodexRunText, codexAppServerErrorMessage, codexAppServerMessageDeferred, codexPromptInputFromRequest, CODEX_APP_SERVER_AGENT_RUN_ID, CODEX_APP_SERVER_RESULT_PROCESSED_EVENT, CODEX_AGENT_PROVIDER, CODEX_APP_SERVER_ACTIVE_RUN_STATES };
@@ -0,0 +1,18 @@
1
+ import { nativeCommandHook } from "./commandWrapper.js";
2
+
3
+ // Codex invokes a command hook; Claude delivers this same input over its SDK
4
+ // control channel. Neither mechanism owns the application's shell policy.
5
+ const chunks = [];
6
+ let size = 0;
7
+ try {
8
+ for await (const chunk of process.stdin) {
9
+ size += chunk.length;
10
+ if (size > 2 * 1024 * 1024) throw new Error("The native command hook input exceeded its size limit.");
11
+ chunks.push(chunk);
12
+ }
13
+ const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
14
+ process.stdout.write(`${JSON.stringify(nativeCommandHook(process.argv[2], input))}\n`);
15
+ } catch {
16
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny",
17
+ permissionDecisionReason: "The host could not bind this native command to its execution wrapper." } })}\n`);
18
+ }
@@ -0,0 +1,37 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const shellQuote = value => `'${value.replaceAll("'", `'"'"'`)}'`;
5
+
6
+ export function validateCommandWrapper(wrapper) {
7
+ if (typeof wrapper !== "string" || !isAbsolute(wrapper) || wrapper.includes("\0")) {
8
+ throw new TypeError("host.commandWrapper must name an absolute executable path.");
9
+ }
10
+ }
11
+
12
+ /** The host executable receives the original command as one argument. */
13
+ export function wrapNativeCommand(wrapper, command) {
14
+ validateCommandWrapper(wrapper);
15
+ if (typeof command !== "string" || !command.trim() || command.includes("\0")) {
16
+ throw new TypeError("The native shell tool requires valid command text.");
17
+ }
18
+ return `${shellQuote(wrapper)} ${shellQuote(command)}`;
19
+ }
20
+
21
+ export function nativeCommandHook(wrapper, input) {
22
+ if (input?.hook_event_name !== "PreToolUse" || input.tool_name !== "Bash") {
23
+ throw new Error("The command hook received an unexpected native tool.");
24
+ }
25
+ try {
26
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow",
27
+ updatedInput: { ...input.tool_input, command: wrapNativeCommand(wrapper, input.tool_input?.command) } } };
28
+ } catch (error) {
29
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny",
30
+ permissionDecisionReason: error.message } };
31
+ }
32
+ }
33
+
34
+ export function codexCommandHookCommand(wrapper) {
35
+ validateCommandWrapper(wrapper);
36
+ return [process.execPath, fileURLToPath(new URL("./commandHook.js", import.meta.url)), wrapper].map(shellQuote).join(" ");
37
+ }
@@ -0,0 +1,43 @@
1
+ import { validateConversationOutputSchema } from "./structuredOutput.js";
2
+
3
+ /** Keep one input shape while retaining explicit connection defaults and native login. */
4
+ export function normalizeConversationConfiguration(configuration, { engine, defaultIntegrationId } = {}) {
5
+ const normalized = { ...configuration };
6
+ for (const field of ["integrationId", "model", "effort", "outputSchema"]) {
7
+ if (normalized[field] === undefined) delete normalized[field];
8
+ }
9
+ if (normalized.integrationId === undefined && defaultIntegrationId !== undefined &&
10
+ (engine === "api" || engine === "opencode")) normalized.integrationId = defaultIntegrationId;
11
+ return normalized;
12
+ }
13
+
14
+ /** One configuration shape; connections authorize models and drivers validate native choices. */
15
+ export function validateConversationConfiguration(configuration, { engine, connections, apiClientFactory, connectionRequired = false,
16
+ structuredOutput = false, maxOutputCharacters,
17
+ efforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"] } = {}) {
18
+ if (!configuration || typeof configuration.systemPrompt !== "string" || !configuration.systemPrompt.trim() ||
19
+ configuration.systemPrompt.length > 128_000 ||
20
+ Object.keys(configuration).some(key => !["systemPrompt", "integrationId", "model", "effort", "outputSchema"].includes(key)) ||
21
+ [configuration.integrationId, configuration.model, configuration.effort].some(value =>
22
+ value !== undefined && (typeof value !== "string" || !value.trim() || value.length > 256))) {
23
+ throw new TypeError("Conversation configuration requires systemPrompt and optional integrationId, model and effort.");
24
+ }
25
+ if ((connectionRequired || configuration.integrationId !== undefined) &&
26
+ (!configuration.integrationId || typeof connections?.resolve !== "function") &&
27
+ typeof apiClientFactory?.resolveClient !== "function") {
28
+ throw new TypeError(`${engine} requires an authorized integrationId and connection resolver.`);
29
+ }
30
+ if (configuration.effort !== undefined && !efforts.includes(configuration.effort)) {
31
+ throw new TypeError(`${engine} does not support the requested reasoning effort.`);
32
+ }
33
+ if (configuration.outputSchema !== undefined) {
34
+ if (!structuredOutput) throw new TypeError(`${engine} does not support structured output.`);
35
+ validateConversationOutputSchema(configuration.outputSchema, { maxOutputCharacters });
36
+ }
37
+ }
38
+
39
+ export function validateConnectionModel(configuration, connection) {
40
+ if (configuration.model && configuration.model !== connection.model) {
41
+ throw new Error("The configured model differs from the authorized connection's model.");
42
+ }
43
+ }