@jskit-ai/assistant-core 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,203 @@
1
+ import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure } from "./codexTurnState.js";
2
+ import { CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS, createCodexAppServerDetachedTurnWatcher } from "./codexDetachedTurn.js";
3
+ import { assertCodexAppServerHelperAccountIdentity } from "./codexHelperExecution.js";
4
+ import { prepareCodexAppServerConversationThread } from "./codexConversationTurns.js";
5
+ import { codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProviderTurns.js";
6
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
7
+
8
+ function ignoreDetachedConversationEvent() {}
9
+
10
+ export async function acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input = {}) {
11
+ let thread = null;
12
+ if (requestedThreadId) {
13
+ thread = await provider.resumeThread(requestedThreadId, threadSettings);
14
+ }
15
+ if (!thread) {
16
+ thread = await provider.startThread({
17
+ ...threadSettings,
18
+ ...(input.ephemeral === true ? { ephemeral: true } : {})
19
+ });
20
+ }
21
+ return thread;
22
+ }
23
+
24
+ export function codexAppServerDetachedThreadId(thread, requestedThreadId) {
25
+ const threadId = normalizeText(thread.id || thread.response?.thread?.id || requestedThreadId);
26
+ if (!threadId) {
27
+ throw new Error("Codex app-server did not return a detached chat thread id.");
28
+ }
29
+ return threadId;
30
+ }
31
+
32
+ export async function dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, authorized) {
33
+ const delivery = await sendCodexAppServerPrompt({ provider, prompt, threadId }, authorized);
34
+ return { delivery, ...codexAppServerDeliveredTurn(delivery) };
35
+ }
36
+
37
+ export async function runDetachedCodexAppServerTurn({
38
+ provider,
39
+ threadId,
40
+ timeoutMs,
41
+ onEvent: emitDetachedEvent,
42
+ onFailure
43
+ } = {}, operation) {
44
+ const watcher = createCodexAppServerDetachedTurnWatcher(provider, threadId, {
45
+ onEvent: (classification) => {
46
+ emitDetachedEvent({
47
+ classification,
48
+ threadId,
49
+ turnId: classification.turnId,
50
+ type: "notification"
51
+ });
52
+ },
53
+ timeoutMs
54
+ });
55
+ const waitForResult = watcher.wait();
56
+ void waitForResult.catch(() => null);
57
+ const throwWatcherFailure = async (fallbackError, status = "", {
58
+ waitForDetail = false
59
+ } = {}) => {
60
+ if (waitForDetail) {
61
+ watcher.failAfterDetailGrace(fallbackError);
62
+ } else {
63
+ watcher.failNow(fallbackError);
64
+ }
65
+ const error = await waitForResult.then(
66
+ () => fallbackError,
67
+ (watcherError) => watcherError
68
+ );
69
+ throw await onFailure(error, status);
70
+ };
71
+ const { delivery, status, turnId } = await operation(throwWatcherFailure);
72
+ watcher.setTurnId(turnId);
73
+ emitDetachedEvent({
74
+ status,
75
+ threadId,
76
+ turnId,
77
+ type: "turn"
78
+ });
79
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
80
+ const error = new Error(codexAppServerDeliveredTurn(delivery).error || `Codex app-server turn ${status}.`);
81
+ await throwWatcherFailure(error, status, {
82
+ waitForDetail: true
83
+ });
84
+ }
85
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
86
+ await watcher.completeNow(status);
87
+ }
88
+ let result = null;
89
+ try {
90
+ result = await waitForResult;
91
+ } catch (error) {
92
+ throw await onFailure(error, status);
93
+ }
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * A detached conversation: one prompt on a resumed or fresh native thread, watched until its turn completes.
99
+ * A Helper's turn runs on its retained Helper thread instead.
100
+ * @param {object} dependencies
101
+ * @param {object} dependencies.conversationPreparation The application's conversation preparation.
102
+ * @param {import("./codexConversations.js").CodexConversationResult} dependencies.codexAppServerConversationResult
103
+ * @param {import("./codexConversations.js").CodexConversationContextReader} dependencies.codexAppServerConversationContext
104
+ * @param {import("./codexHelperThreads.js").CodexHelperThreadLifecycle} dependencies.helperLifecycle
105
+ * @param {string} dependencies.helperOwnershipError
106
+ */
107
+ export function createCodexDetachedConversation({
108
+ conversationPreparation,
109
+ codexAppServerConversationResult,
110
+ codexAppServerConversationContext,
111
+ helperLifecycle,
112
+ helperOwnershipError
113
+ }) {
114
+ async function runDetachedCodexAppServerConversation(sessionId, input = {}, options = {}) {
115
+ const preparation = conversationPreparation.detached(sessionId, input);
116
+ const { admission } = preparation;
117
+ if (admission.ok === false) return admission;
118
+ try {
119
+ return await codexAppServerConversationResult(async () => {
120
+ const unavailable = preparation.result;
121
+ if (unavailable) return unavailable;
122
+ const prompt = preparation.prompt;
123
+ const context = await codexAppServerConversationContext(sessionId, input, options);
124
+ if (context.ok === false) return context;
125
+ const { provider, runtime, workdir } = context;
126
+ const executionProfile = input.executionProfile && typeof input.executionProfile === "object" &&
127
+ !Array.isArray(input.executionProfile) ? input.executionProfile : null;
128
+ const helperTurn = Boolean(executionProfile);
129
+ const emitDetachedEvent = options.onEvent || ignoreDetachedConversationEvent;
130
+ const onRetired = ({ threadId }) => emitDetachedEvent({ threadId, type: "thread-retired" });
131
+ if (helperTurn) {
132
+ await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
133
+ }
134
+ const execution = preparation.execution(context);
135
+ const threadSettings = helperTurn ? null : await prepareCodexAppServerConversationThread(
136
+ provider, execution.threadPreparation, conversationPreparation.isolation
137
+ );
138
+ const requestedThreadId = normalizeText(input.threadId || input.codexSessionId);
139
+ let helper = null;
140
+ let thread;
141
+ if (helperTurn) {
142
+ const scope = { executionProfile, onRetired, projectRuntimeRoot: normalizeText(runtime?.stateRoot),
143
+ projectContextRoot: normalizeText(runtime?.projectContextRoot), provider, sessionId, requestedThreadId, workdir,
144
+ ...(requestedThreadId ? { record: helperLifecycle.threadForOperation({ executionProfile,
145
+ projectRuntimeRoot: normalizeText(runtime?.stateRoot), provider, sessionId,
146
+ threadId: requestedThreadId, workdir }) } : {}) };
147
+ helper = await helperLifecycle.prepareDetached(scope, execution.helperPreparation);
148
+ thread = helper.thread;
149
+ } else {
150
+ thread = await acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input);
151
+ }
152
+ const threadId = codexAppServerDetachedThreadId(thread, requestedThreadId);
153
+ emitDetachedEvent({ threadId, type: "thread" });
154
+ const requestedTimeoutMs = Number(input.timeoutMs || 0);
155
+ const profileTimeoutMs = Number(executionProfile?.limits?.timeoutMs || 0);
156
+ let turnId = "";
157
+ const result = await runDetachedCodexAppServerTurn({
158
+ provider, threadId, onEvent: emitDetachedEvent,
159
+ timeoutMs: helperTurn
160
+ ? Math.min(requestedTimeoutMs > 0 ? requestedTimeoutMs : profileTimeoutMs, profileTimeoutMs)
161
+ : requestedTimeoutMs > 0 ? requestedTimeoutMs : CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
162
+ async onFailure(error, status) {
163
+ await helper?.discard();
164
+ return execution.failure(error, status);
165
+ }
166
+ }, async failDispatch => {
167
+ let dispatched;
168
+ if (helperTurn) {
169
+ dispatched = await helper.dispatch(threadId, failDispatch);
170
+ } else {
171
+ try {
172
+ dispatched = await dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, execution.authorized);
173
+ } catch (error) {
174
+ await failDispatch(error);
175
+ }
176
+ }
177
+ turnId = dispatched.turnId;
178
+ return dispatched;
179
+ });
180
+ if (helperTurn) {
181
+ try {
182
+ await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
183
+ execution.validateOutput(result.text);
184
+ } catch (error) {
185
+ await helper.discard();
186
+ throw error;
187
+ }
188
+ await helper.complete();
189
+ }
190
+ emitDetachedEvent({ status: result.status || "completed", text: result.text,
191
+ threadId, turnId: result.turnId || turnId, type: "completed" });
192
+ return { ok: true, text: result.text, threadId, turnId: result.turnId || turnId,
193
+ ...(helperTurn ? { inputCharacters: prompt.length, outputCharacters: result.text.length, usage: result.usage || null } : {}) };
194
+ });
195
+ } finally {
196
+ admission.release();
197
+ }
198
+ }
199
+
200
+ return Object.freeze({ runDetachedCodexAppServerConversation });
201
+ }
202
+
203
+ /** @typedef {ReturnType<typeof createCodexDetachedConversation>} CodexDetachedConversation */
@@ -1,6 +1,14 @@
1
1
  import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
2
2
  import { normalizeNullableText } from "./normalize.js";
3
- import { classifyCodexAppServerEvent, codexAppServerNotificationParams, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerNotificationError, codexAppServerStatusFromValue, codexAppServerProviderThreadAssistantSegments, codexAppServerProviderTurnId, codexAppServerProviderTurnStatus, codexAppServerProviderTurnForOperation, codexAppServerProviderTurnText, codexAppServerProviderTurnError } from "./codexEvents.js";
3
+ import { classifyCodexAppServerEvent, codexAppServerNotificationParams, codexAppServerNotificationThreadId, codexAppServerNotificationTurnId, codexAppServerNotificationTurnStatus, codexAppServerNotificationError, codexAppServerStatusFromValue } from "./codexEvents.js";
4
+ import {
5
+ codexAppServerProviderThreadAssistantSegments,
6
+ codexAppServerProviderTurnId,
7
+ codexAppServerProviderTurnStatus,
8
+ codexAppServerProviderTurnForOperation,
9
+ codexAppServerProviderTurnText,
10
+ codexAppServerProviderTurnError
11
+ } from "./codexThreadSnapshot.js";
4
12
  import { codexAppServerTurnStatusIsProviderFailure, codexAppServerTurnStatusIsSuccessfulComplete } from "./codexTurnState.js";
5
13
 
6
14
  const CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS = 180_000;
@@ -30,10 +38,33 @@ function codexAppServerTurnTokenUsage(notification = {}) {
30
38
  return Object.keys(snapshot).length ? Object.freeze(snapshot) : null;
31
39
  }
32
40
 
33
- function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "", {
41
+ /**
42
+ * The provider methods a detached turn watcher reads.
43
+ * @typedef {Pick<import("./codexProvider.js").CodexAppServerAgentProvider,
44
+ * "subscribe" | "readThread" | "isAvailable" | "currentConnectionGeneration">} CodexDetachedTurnProvider
45
+ */
46
+
47
+ /**
48
+ * The observer told about each classified notification of the watched turn.
49
+ * @typedef {(classification: object, notification: object) => void} CodexDetachedTurnEventObserver
50
+ */
51
+
52
+ function ignoreCodexDetachedTurnEvent() {}
53
+
54
+ /**
55
+ * Watches one native turn of a thread until it completes, fails or times out.
56
+ * @param {CodexDetachedTurnProvider} provider
57
+ * @param {string} threadId
58
+ * @param {object} [options]
59
+ * @param {boolean} [options.includeThreadHistory]
60
+ * @param {boolean} [options.allowEmptyResponse]
61
+ * @param {CodexDetachedTurnEventObserver} [options.onEvent]
62
+ * @param {number} [options.timeoutMs]
63
+ */
64
+ function createCodexAppServerDetachedTurnWatcher(provider, threadId = "", {
34
65
  includeThreadHistory = true,
35
66
  allowEmptyResponse = false,
36
- onEvent = null,
67
+ onEvent = ignoreCodexDetachedTurnEvent,
37
68
  timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS
38
69
  } = {}) {
39
70
  const normalizedThreadId = normalizeNullableText(threadId);
@@ -79,8 +110,8 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
79
110
  rejectWaiter?.(error);
80
111
  }
81
112
 
82
- function emitWatcherEvent(classification = {}, notification) {
83
- if (typeof onEvent !== "function" || !classification?.kind) {
113
+ function emitWatcherEvent(classification, notification) {
114
+ if (!classification.kind) {
84
115
  return;
85
116
  }
86
117
  onEvent({
@@ -94,8 +125,7 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
94
125
  if (
95
126
  !includeThreadHistory ||
96
127
  !normalizedThreadId ||
97
- !targetTurnId ||
98
- typeof provider?.readThread !== "function"
128
+ !targetTurnId
99
129
  ) {
100
130
  return {
101
131
  status: "",
@@ -165,104 +195,101 @@ function createCodexAppServerDetachedTurnWatcher(provider = null, threadId = "",
165
195
  return !targetTurnId || !notificationTurnId || notificationTurnId === targetTurnId;
166
196
  }
167
197
 
168
- return {
169
- async completeNow(status = "completed") {
170
- await finishFromCompletion(status);
171
- },
172
- failNow(error) {
173
- fail(error);
174
- },
175
- failAfterDetailGrace(error) {
176
- failAfterDetailGrace(error);
177
- },
178
- setTurnId(turnId = "") {
179
- targetTurnId = normalizeNullableText(turnId);
180
- if (pendingFailure) {
181
- failAfterDetailGrace(pendingFailure);
182
- return;
183
- }
184
- if (pendingCompletionStatus) {
185
- const status = pendingCompletionStatus;
186
- pendingCompletionStatus = "";
187
- void finishFromCompletion(status);
188
- }
189
- },
190
- wait() {
191
- if (settled) {
192
- return Promise.reject(new Error("Codex app-server detached turn watcher was already settled."));
198
+ function setTurnId(turnId = "") {
199
+ targetTurnId = normalizeNullableText(turnId);
200
+ if (pendingFailure) {
201
+ failAfterDetailGrace(pendingFailure);
202
+ return;
203
+ }
204
+ if (pendingCompletionStatus) {
205
+ const status = pendingCompletionStatus;
206
+ pendingCompletionStatus = "";
207
+ void finishFromCompletion(status);
208
+ }
209
+ }
210
+
211
+ function wait() {
212
+ if (settled) {
213
+ return Promise.reject(new Error("Codex app-server detached turn watcher was already settled."));
214
+ }
215
+ return new Promise((resolve, reject) => {
216
+ resolveWaiter = resolve;
217
+ rejectWaiter = reject;
218
+ if (timeoutMs > 0) {
219
+ timeout = setTimeout(() => {
220
+ fail(new Error("Timed out waiting for Codex app-server response."));
221
+ }, timeoutMs);
222
+ } else {
223
+ // Interactive repair turns may take longer than bounded helper jobs.
224
+ // Retain their watcher until completion, interruption, or connection loss.
225
+ const generation = provider.currentConnectionGeneration();
226
+ connectionCheck = setInterval(() => {
227
+ if (provider.isAvailable() === false ||
228
+ provider.currentConnectionGeneration() !== generation) {
229
+ fail(new Error("Connection to Codex was lost while the temporary AI was working."));
230
+ }
231
+ }, 1000);
232
+ connectionCheck.unref();
193
233
  }
194
- return new Promise((resolve, reject) => {
195
- resolveWaiter = resolve;
196
- rejectWaiter = reject;
197
- if (timeoutMs > 0) {
198
- timeout = setTimeout(() => {
199
- fail(new Error("Timed out waiting for Codex app-server response."));
200
- }, timeoutMs);
201
- } else {
202
- // Interactive repair turns may take longer than bounded helper jobs.
203
- // Retain their watcher until completion, interruption, or connection loss.
204
- const generation = provider?.currentConnectionGeneration?.();
205
- connectionCheck = setInterval(() => {
206
- if (provider?.isAvailable?.() === false ||
207
- provider?.currentConnectionGeneration?.() !== generation) {
208
- fail(new Error("Connection to Codex was lost while the temporary AI was working."));
209
- }
210
- }, 1000);
211
- connectionCheck.unref?.();
234
+ unsubscribe = provider.subscribe((notification = {}) => {
235
+ if (!notificationMatches(notification)) {
236
+ return;
237
+ }
238
+ usage = codexAppServerTurnTokenUsage(notification) || usage;
239
+ const classification = classifyCodexAppServerEvent(notification);
240
+ emitWatcherEvent(classification, notification);
241
+ if (
242
+ classification.kind === "provider_error" &&
243
+ classification.text &&
244
+ codexAppServerNotificationParams(notification).willRetry !== true
245
+ ) {
246
+ const error = new Error(classification.text);
247
+ if (!targetTurnId) {
248
+ pendingFailure = error;
249
+ return;
250
+ }
251
+ fail(error);
252
+ return;
253
+ }
254
+ if (classification.kind === "final_assistant_result" && classification.text) {
255
+ finalText = classification.text;
256
+ if (pendingCompletionStatus && targetTurnId) {
257
+ const status = pendingCompletionStatus;
258
+ pendingCompletionStatus = "";
259
+ clearTimeout(failureDetailTimeout);
260
+ failureDetailTimeout = null;
261
+ void finishFromCompletion(status);
262
+ }
263
+ }
264
+ const method = normalizeNullableText(notification.method);
265
+ if (method !== "turn/completed" && method !== "thread/status/changed") {
266
+ return;
267
+ }
268
+ const status = codexAppServerNotificationTurnStatus(notification) || "completed";
269
+ if (codexAppServerTurnStatusIsProviderFailure(status)) {
270
+ const error = new Error(codexAppServerNotificationError(notification) || `Codex app-server turn ${status}.`);
271
+ if (!targetTurnId) {
272
+ pendingFailure = error;
273
+ return;
274
+ }
275
+ failAfterDetailGrace(error);
276
+ return;
277
+ }
278
+ if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
279
+ void finishFromCompletion(status);
212
280
  }
213
- unsubscribe = typeof provider?.subscribe === "function"
214
- ? provider.subscribe((notification = {}) => {
215
- if (!notificationMatches(notification)) {
216
- return;
217
- }
218
- usage = codexAppServerTurnTokenUsage(notification) || usage;
219
- const classification = classifyCodexAppServerEvent(notification);
220
- emitWatcherEvent(classification, notification);
221
- if (
222
- classification.kind === "provider_error" &&
223
- classification.text &&
224
- codexAppServerNotificationParams(notification).willRetry !== true
225
- ) {
226
- const error = new Error(classification.text);
227
- if (!targetTurnId) {
228
- pendingFailure = error;
229
- return;
230
- }
231
- fail(error);
232
- return;
233
- }
234
- if (classification.kind === "final_assistant_result" && classification.text) {
235
- finalText = classification.text;
236
- if (pendingCompletionStatus && targetTurnId) {
237
- const status = pendingCompletionStatus;
238
- pendingCompletionStatus = "";
239
- clearTimeout(failureDetailTimeout);
240
- failureDetailTimeout = null;
241
- void finishFromCompletion(status);
242
- }
243
- }
244
- const method = normalizeNullableText(notification.method);
245
- if (method !== "turn/completed" && method !== "thread/status/changed") {
246
- return;
247
- }
248
- const status = codexAppServerNotificationTurnStatus(notification) || "completed";
249
- if (codexAppServerTurnStatusIsProviderFailure(status)) {
250
- const error = new Error(codexAppServerNotificationError(notification) || `Codex app-server turn ${status}.`);
251
- if (!targetTurnId) {
252
- pendingFailure = error;
253
- return;
254
- }
255
- failAfterDetailGrace(error);
256
- return;
257
- }
258
- if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
259
- void finishFromCompletion(status);
260
- }
261
- })
262
- : null;
263
281
  });
264
- }
265
- };
282
+ });
283
+ }
284
+
285
+ return Object.freeze({
286
+ completeNow: finishFromCompletion,
287
+ failNow: fail,
288
+ failAfterDetailGrace,
289
+ // readable-check: allow setter-seam - not a slot: setTurnId records the turn identity that startup acknowledges after wait() has subscribed
290
+ setTurnId,
291
+ wait
292
+ });
266
293
  }
267
294
 
268
295
  function codexAppServerTurnError(reason, details = {}, providerMessage = "") {
@@ -276,6 +303,15 @@ function codexAppServerTurnError(reason, details = {}, providerMessage = "") {
276
303
  return error;
277
304
  }
278
305
 
306
+ /**
307
+ * Waits for an already started native turn and returns its final answer.
308
+ * @param {CodexDetachedTurnProvider} provider
309
+ * @param {string} threadId
310
+ * @param {object | null} turn
311
+ * @param {object} [options]
312
+ * @param {number} [options.timeoutMs]
313
+ * @param {typeof codexAppServerTurnError} [options.createError]
314
+ */
279
315
  async function waitForCodexAppServerTurn(provider, threadId = "", turn = null, {
280
316
  timeoutMs = CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
281
317
  createError = codexAppServerTurnError
@@ -313,7 +349,7 @@ async function waitForCodexAppServerTurn(provider, threadId = "", turn = null, {
313
349
  void pending.catch(() => null);
314
350
  if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
315
351
  await watcher.completeNow(status);
316
- } else if (typeof provider?.readThread === "function") {
352
+ } else {
317
353
  const latestThread = await provider.readThread(normalizedThreadId);
318
354
  const latestTurn = codexAppServerProviderTurnForOperation(latestThread, { turnId });
319
355
  const latestStatus = codexAppServerProviderTurnStatus(latestTurn || {});
@@ -330,4 +366,9 @@ async function waitForCodexAppServerTurn(provider, threadId = "", turn = null, {
330
366
  return pending;
331
367
  }
332
368
 
333
- export { CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS, createCodexAppServerDetachedTurnWatcher, waitForCodexAppServerTurn };
369
+ export {
370
+ CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
371
+ createCodexAppServerDetachedTurnWatcher,
372
+ ignoreCodexDetachedTurnEvent,
373
+ waitForCodexAppServerTurn
374
+ };