@jskit-ai/assistant-core 0.2.7 → 0.2.9

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,708 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import { claudeMessageBlocks } from "./claudeHistory.js";
3
+ import { stopClaudeCodeProcess } from "./claudeProcess.js";
4
+ import { createClaudeConversationAdapter } from "./providers/claude.js";
5
+
6
+ export function claudeTurnFailure(message, { code = "assistant_claude_turn_failed" } = {}) {
7
+ return Object.assign(new Error(message), { code });
8
+ }
9
+
10
+ // Input for a turn that already finished is the caller's refusal; input that
11
+ // meets a stopping turn stays an internal turn failure.
12
+ function steeringRefusal(message, { messageCode, finished }) {
13
+ return finished
14
+ ? new AppError(409, message, { code: "conversation_steer_turn_finished", messageCode })
15
+ : Object.assign(claudeTurnFailure(message), { messageCode, messageParams: {} });
16
+ }
17
+
18
+ const CLAUDE_SIGNED_OUT_MESSAGE = "Claude isn't connected. Sign in to Claude, then send your message again.";
19
+
20
+ function setClaudeTurnState(entry, state, message = "") {
21
+ if (!entry.turn) return;
22
+ const active = ["starting", "active", "finalizing"].includes(state);
23
+ Object.assign(entry.turn, { active, state, error: message, updatedAt: new Date().toISOString(),
24
+ phase: active && !entry.observationError ? String(entry.turn.phase ?? "").trim() : "" });
25
+ }
26
+
27
+ // The entry's turn result, with the text and messages the native view holds.
28
+ function claudeTurnResult(entry, current) {
29
+ const failed = entry.turn?.state === "failed";
30
+ let status = "completed";
31
+ if (entry.turn?.active) status = "inProgress";
32
+ else if (failed) status = "failed";
33
+ else if (entry.turn?.state === "interrupted") status = "interrupted";
34
+ return { ok: !failed, conversationId: entry.id, threadId: entry.id,
35
+ runId: entry.turn?.id || "", turnId: entry.turn?.id || "", text: current.text,
36
+ messages: current.messages, error: entry.turn?.error || "", status };
37
+ }
38
+
39
+ // The provider state a native frame changes: its goal or its usage.
40
+ function claudeProviderChange(frame) {
41
+ return frame.type === "active_goal" ? "goal" : frame.type === "rate_limit_event" ? "usage" : "";
42
+ }
43
+
44
+ // A frame that shows Claude working on this turn again.
45
+ function continuesClaudeTurn(frame) {
46
+ return (frame.type === "user" && frame.origin?.kind === "task-notification") ||
47
+ (frame.type === "command_lifecycle" && frame.state === "started") ||
48
+ (frame.type === "stream_event" && frame.event?.type === "message_start") || frame.type === "assistant";
49
+ }
50
+
51
+ // The native background tasks a system frame starts, lists or finishes.
52
+ function updateClaudeBackgroundTasks(tasks, frame) {
53
+ if (frame.subtype === "background_tasks_changed") {
54
+ tasks.clear();
55
+ for (const task of frame.tasks || []) if (task.task_id && !task.ambient) tasks.add(task.task_id);
56
+ }
57
+ if (frame.subtype === "task_started" && frame.task_id && !frame.ambient) tasks.add(frame.task_id);
58
+ if (["task_notification", "task_updated"].includes(frame.subtype) &&
59
+ ["completed", "failed", "stopped", "killed"].includes(frame.status || frame.patch?.status)) {
60
+ tasks.delete(frame.task_id);
61
+ }
62
+ }
63
+
64
+ // Native split frames arrive before their block's stop event. The one live
65
+ // block a frame continues keeps that identity without replacing the saved frame UUID.
66
+ function liveClaudeBlock(messages, messageId, frame, role) {
67
+ if (typeof messageId === "string" && messageId && frame.message.id === messageId && frame.message.content.length === 1) {
68
+ const prefix = `claude_${messageId}_`;
69
+ const live = [...messages.values()].filter(message => !message.complete && message.role === role &&
70
+ message.id.startsWith(prefix) && /^\d+$/u.test(message.id.slice(prefix.length)));
71
+ if (live.length === 1 && (!Object.hasOwn(frame, "apiBlockIndex") ||
72
+ Number.isSafeInteger(frame.apiBlockIndex) && frame.apiBlockIndex >= 0 &&
73
+ live[0].id === `${prefix}${frame.apiBlockIndex}`)) return live[0];
74
+ }
75
+ return null;
76
+ }
77
+
78
+ // A conversation whose account signed out under it ends its next request
79
+ // at once with Claude Code's own "Not logged in" answer.
80
+ function claudeResultError(frame, output) {
81
+ return /^Not logged in\b/u.test(output) ? CLAUDE_SIGNED_OUT_MESSAGE
82
+ : (frame.errors || []).join("; ") || output || frame.subtype;
83
+ }
84
+
85
+ // The final answer a result frame carries, with the local command that wrote it.
86
+ function claudeResultMessage(frame, turnId, result) {
87
+ return { id: `claude_${frame.uuid || turnId}_result`, role: "assistant", text: result, complete: true,
88
+ ...(typeof frame.local_command === "string" && frame.local_command ? {
89
+ data: { native: { engineId: "claude", type: "result", command: frame.local_command, output: "stdout" } }
90
+ } : {}) };
91
+ }
92
+
93
+ // Verified process cleanup can precede Claude's terminal generation frame.
94
+ // Retain already displayed text for the transcript reader, with each block's
95
+ // input/output identity and no completed-answer receipt.
96
+ function retainStoppedClaudeMessages(messages, pendingAssistantMessages) {
97
+ for (const pending of pendingAssistantMessages.values()) for (const block of pending) {
98
+ if (block.outputId && block.outputId !== block.id) messages.delete(block.outputId);
99
+ messages.set(block.id, { ...block, role: "commentary" });
100
+ }
101
+ for (const [id, block] of messages) {
102
+ if (block.complete === false && block.role === "assistant" && block.text) {
103
+ messages.set(id, { ...block, complete: true, role: "commentary" });
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The owner's control of one native process, as a native turn uses it.
110
+ * @typedef {import("./providers/claude.js").ClaudeProcessControl & {
111
+ * stopExecution?: (executionId: string, options?: object) => Promise<object>,
112
+ * releaseExecution?: () => Promise<unknown>
113
+ * }} ClaudeTurnProcess
114
+ */
115
+
116
+ /**
117
+ * One native Claude turn: its admission, events and results; hosts persist its events.
118
+ * A turn with an owner's retained entry also reads results and stops its process.
119
+ * @param {{
120
+ * conversationId: string,
121
+ * onEvent: (event: Record<string, any>) => unknown,
122
+ * process?: ClaudeTurnProcess,
123
+ * entry?: Record<string, any>,
124
+ * bound?: boolean
125
+ * }} options
126
+ */
127
+ export function createClaudeConversationTurn({ conversationId, onEvent, process, entry, bound = false }) {
128
+ if (!conversationId) throw new TypeError("A Claude conversation id is required.");
129
+ const instructions = process ? createClaudeConversationAdapter(process) : null;
130
+ const publish = entry ? publishEntryEvent : onEvent;
131
+ const messages = new Map();
132
+ const pendingAssistantMessages = new Map();
133
+ const assistantRoles = new Map();
134
+ const admissions = new Map();
135
+ const inFlight = new Set();
136
+ // Inputs Claude has begun reading. A queued steer joins the running loop, so
137
+ // one result can finish several of them; queued-only input stays in flight.
138
+ const startedInputs = new Set();
139
+ // Steers written into a running loop that Claude has not begun reading. Claude
140
+ // Code reads queued input only when the running tool call returns.
141
+ const waitingSteers = new Set();
142
+ let steeringClient = null;
143
+ const tasks = new Set();
144
+ let turnId = "";
145
+ let messageId = "";
146
+ let currentCommandId = "";
147
+ let result = "";
148
+ let phase = "";
149
+ let outcome;
150
+ let active = false;
151
+ let generation = 0;
152
+ let stopped = false;
153
+ let cancelling = false;
154
+ let maximumOutput = 4 * 1024 * 1024;
155
+ let inputOwner = null;
156
+ let acceptedInput = null;
157
+ let publishedAssistant = null;
158
+ let finalAssistantResult = null;
159
+ let resolvedCompletion;
160
+ let latestSettlement;
161
+ // A common command fences its first checkpoint; later native continuations
162
+ // keep the same input but cannot enqueue onto that already resolved command.
163
+ let checkpointedCommand;
164
+
165
+ function clearFinalAssistantResult() {
166
+ publishedAssistant = null;
167
+ finalAssistantResult = null;
168
+ }
169
+
170
+ function invalidateFinalAssistantResult() {
171
+ inputOwner = null;
172
+ acceptedInput = null;
173
+ clearFinalAssistantResult();
174
+ }
175
+
176
+ function clearTurnView() {
177
+ messages.clear();
178
+ pendingAssistantMessages.clear();
179
+ assistantRoles.clear();
180
+ result = "";
181
+ outcome = null;
182
+ invalidateFinalAssistantResult();
183
+ }
184
+
185
+ // A waiting steer would otherwise sit behind a long foreground command (or a
186
+ // wait loop) until it returns. Claude's Ctrl+B control moves foreground Bash
187
+ // and subagents to the background: they keep running and report completion,
188
+ // and the blocked tool call returns so Claude reads the steer now.
189
+ function backgroundForegroundWork() {
190
+ const client = steeringClient;
191
+ if (!waitingSteers.size || !client?.request || stopped || cancelling) return;
192
+ void client.request({ subtype: "background_tasks" }).then(
193
+ () => publish({ type: "diagnostic", stage: "steering_backgrounded", pendingCommands: inFlight.size, backgroundTasks: tasks.size }),
194
+ // An older CLI still reads the steer when its tool call returns.
195
+ error => publish({ type: "diagnostic", stage: "steering_background_failed", error: error.message })
196
+ ).catch(() => {});
197
+ }
198
+
199
+ function currentReceiptOwner(owner) {
200
+ return Boolean(entry && owner && inputOwner === owner && acceptedInput === owner &&
201
+ owner.commandId === currentCommandId && entry.process && entry.process === owner.process &&
202
+ entry.executionId === owner.executionId && entry.accountIdentity === owner.accountIdentity &&
203
+ entry.turn?.id === turnId && !entry.stopping && !entry.stopPending && !entry.disposed &&
204
+ !entry.observationError && !entry.command?.toolWork.size && !entry.command?.toolFailure);
205
+ }
206
+
207
+ function readFinalAssistantResult(threadId, expectedTurnId) {
208
+ if (threadId !== conversationId || expectedTurnId !== turnId || !finalAssistantResult ||
209
+ active || stopped || cancelling || outcome?.status !== "completed" || outcome.error ||
210
+ inFlight.size || tasks.size || admissions.size || !currentReceiptOwner(acceptedInput)) return null;
211
+ return structuredClone(finalAssistantResult);
212
+ }
213
+
214
+ function read() {
215
+ return { turnId, active, text: result, phase, status: active ? "inProgress" : outcome?.status || "ready",
216
+ error: outcome?.error || "", messages: structuredClone([...messages.values()]),
217
+ pendingCommands: inFlight.size, backgroundTasks: tasks.size };
218
+ }
219
+
220
+ function readResult() {
221
+ return claudeTurnResult(entry, read());
222
+ }
223
+
224
+ function resolveCompletion(result) {
225
+ entry.completion?.resolve(result);
226
+ resolvedCompletion = entry.completion;
227
+ if (entry.command?.finishIfCurrent) entry.command.completion.resolve(result);
228
+ }
229
+
230
+ async function updateState(state, message = "", expectedGeneration = generation) {
231
+ if (!entry.turn || expectedGeneration !== generation) return false;
232
+ const active = ["starting", "active", "finalizing"].includes(state);
233
+ const checkpoint = onEvent({ type: "before-state", state, active, message });
234
+ if (checkpoint) await checkpoint;
235
+ if (expectedGeneration !== generation) return false;
236
+ setClaudeTurnState(entry, state, message);
237
+ await onEvent({ type: "save" });
238
+ if (expectedGeneration !== generation) return false;
239
+ await onEvent({ type: "state", state, active, message });
240
+ return expectedGeneration === generation;
241
+ }
242
+
243
+ async function stopProcess(reason = "", { preparing = false } = {}) {
244
+ clearTimeout(entry.goalToolStop?.timer);
245
+ entry.goalToolStop = null;
246
+ if (!preparing) entry.preparationRevision++;
247
+ invalidateFinalAssistantResult();
248
+ // Runtime bindings commit execution release before dropping the handle.
249
+ // Retained host entries keep their original post-proof checkpoint order.
250
+ if (entry.stopPending) return entry.stopPending;
251
+ const active = read().active;
252
+ if (!bound) entry.stopping = true;
253
+ const operation = async () => {
254
+ const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
255
+ let proof;
256
+ try {
257
+ if (!bound || entry.process || entry.executionId) {
258
+ proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
259
+ stopExecution: process.stopExecution });
260
+ }
261
+ } catch (error) {
262
+ if (bound || error.code !== "claude_stop_unconfirmed") throw error;
263
+ entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
264
+ await onEvent({ type: "save" });
265
+ await onEvent({ type: "stop-unconfirmed" });
266
+ throw error;
267
+ }
268
+ if (bound && (entry.process || entry.executionId)) await process.releaseExecution();
269
+ entry.process = null;
270
+ entry.executionId = "";
271
+ entry.exitProof = proof;
272
+ stop(bound ? reason : reason || "Claude stopped before acknowledging the prompt.");
273
+ if (bound) {
274
+ if (entry.startupCleanup === startup) entry.startupCleanup = null;
275
+ if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
276
+ return;
277
+ }
278
+ if (entry.turn?.active) {
279
+ try { await updateState("interrupted", reason); }
280
+ catch (error) {
281
+ // Native cleanup is verified even when the host refuses its checkpoint.
282
+ // Retain that refusal without publishing a completed answer or active work.
283
+ setClaudeTurnState(entry, "interrupted", error.message);
284
+ await onEvent({ type: "save" });
285
+ await onEvent({ type: "state", state: "interrupted", active: false, message: error.message });
286
+ resolveCompletion(readResult());
287
+ if (entry.startupCleanup === startup) entry.startupCleanup = null;
288
+ throw error;
289
+ }
290
+ } else await onEvent({ type: "save" });
291
+ resolveCompletion(readResult());
292
+ if (entry.startupCleanup === startup) entry.startupCleanup = null;
293
+ return entry.exitProof;
294
+ };
295
+ entry.stopPending = operation().catch(error => { if (bound) error.cleanupFailed = true; throw error; })
296
+ .finally(() => { entry.stopPending = null; });
297
+ return entry.stopPending;
298
+ }
299
+
300
+ async function publishEntryEvent(event, deferred = false) {
301
+ if (entry.stopping) return;
302
+ if (event.type === "settled" && event.generation !== generation) return;
303
+ const command = entry.command;
304
+ if (event.type === "settled" && !bound && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
305
+ const delivery = command.delivery;
306
+ const completion = entry.completion;
307
+ const owner = acceptedInput;
308
+ // Release the native queue before the common run joins reserved steering.
309
+ // That same accepted delivery then performs the original retained checkpoint.
310
+ command.completion.resolve(async () => {
311
+ try {
312
+ for (;;) {
313
+ if (entry.command !== command || command.delivery !== delivery || entry.completion !== completion ||
314
+ command.signal.aborted || !currentReceiptOwner(owner)) {
315
+ throw claudeTurnFailure("Claude's completion belongs to a retired native input.");
316
+ }
317
+ // Follow-ups share this input, but can finish while its checkpoint
318
+ // awaits storage. Apply the latest settlement after the same fence.
319
+ if (active) return readResult();
320
+ const settlement = latestSettlement;
321
+ await publishEntryEvent(settlement, true);
322
+ if (settlement === latestSettlement) return readResult();
323
+ }
324
+ } catch (error) {
325
+ clearFinalAssistantResult();
326
+ throw error;
327
+ } finally {
328
+ checkpointedCommand = command;
329
+ }
330
+ });
331
+ return;
332
+ }
333
+ if (event.type === "continued") {
334
+ if (entry.completion === resolvedCompletion) entry.completion = Promise.withResolvers();
335
+ if (bound) setClaudeTurnState(entry, "active");
336
+ else await updateState("active");
337
+ } else if (event.type === "admitted") {
338
+ if (event.userEcho) entry.sent = true;
339
+ if (!bound) await onEvent({ type: "save" });
340
+ } else if (event.type === "phase") {
341
+ entry.turn.phase = event.phase;
342
+ if (!bound) await updateState(entry.turn.state);
343
+ } else if (event.type === "settled") {
344
+ const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
345
+ if (bound) setClaudeTurnState(entry, state, event.error);
346
+ else if (!await updateState(state, event.error, event.generation)) return;
347
+ }
348
+ const published = await onEvent(event);
349
+ const change = event.type === "provider-event" ? claudeProviderChange(event.event) : "";
350
+ if (change) await onEvent({ type: "provider-update", change });
351
+ if (event.type === "settled") {
352
+ if (event.generation !== generation) return;
353
+ if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
354
+ resolveCompletion(bound ? event : readResult());
355
+ }
356
+ return published;
357
+ }
358
+
359
+ function checkOutput(value, role = "assistant") {
360
+ if (value.length > (role === "thinking" ? 4 * 1024 * 1024 : maximumOutput)) {
361
+ throw claudeTurnFailure("Claude output exceeded its size limit.");
362
+ }
363
+ }
364
+
365
+ async function publishMessage(message) {
366
+ if (!message.userId && currentCommandId) message = { ...message, userId: currentCommandId };
367
+ messages.set(message.id, message);
368
+ const owner = acceptedInput;
369
+ const completeAssistant = message.complete && message.role === "assistant";
370
+ if (completeAssistant) clearFinalAssistantResult();
371
+ const conversationTurn = await publish({ type: "message", message });
372
+ if (completeAssistant && message.text && currentReceiptOwner(owner) && !stopped && !cancelling &&
373
+ conversationTurn && typeof conversationTurn === "object" && !Array.isArray(conversationTurn)) {
374
+ publishedAssistant = { owner, itemId: message.id, outputId: message.outputId || message.id,
375
+ text: message.text, conversationTurn: structuredClone(conversationTurn) };
376
+ }
377
+ return conversationTurn;
378
+ }
379
+
380
+ async function flushAssistantMessages(apiMessageId, role) {
381
+ assistantRoles.set(apiMessageId, role);
382
+ const pending = pendingAssistantMessages.get(apiMessageId) || [];
383
+ pendingAssistantMessages.delete(apiMessageId);
384
+ for (const block of pending) {
385
+ await publishMessage({ ...block, role });
386
+ if (block.outputId) {
387
+ if (block.outputId !== block.id) messages.delete(block.outputId);
388
+ await publish({ type: "message-complete", messageId: block.outputId });
389
+ }
390
+ }
391
+ }
392
+
393
+ async function receive(frame) {
394
+ if (stopped) return;
395
+ if (frame.session_id && frame.session_id !== conversationId) throw claudeTurnFailure("Claude returned a different conversation id.");
396
+ if (frame.parent_tool_use_id) return; // Nested agents do not replace the main reply.
397
+ await publish({ type: "provider-event", event: frame });
398
+ if (continuesClaudeTurn(frame) && turnId && !cancelling) {
399
+ const wasActive = active;
400
+ if (outcome || !active) {
401
+ generation++;
402
+ outcome = null;
403
+ result = "";
404
+ clearFinalAssistantResult();
405
+ }
406
+ active = true;
407
+ if (!wasActive) await publish({ type: "continued", source: frame.type });
408
+ if (phase === "background") {
409
+ phase = "";
410
+ await publish({ type: "phase", phase });
411
+ }
412
+ }
413
+ if (frame.type === "command_lifecycle" || frame.type === "user") {
414
+ const uuid = frame.type === "user" ? frame.uuid : frame.command_uuid;
415
+ if (frame.type === "user" && !inFlight.has(uuid) && !admissions.has(uuid)) return;
416
+ // Native notifications have their own lifecycle UUIDs, including work
417
+ // folded into this input. They must not steal its receipt/result owner.
418
+ if ((frame.state === "started" || frame.type === "user") && inFlight.has(uuid)) {
419
+ currentCommandId = uuid;
420
+ startedInputs.add(uuid);
421
+ }
422
+ if (frame.state !== "queued") waitingSteers.delete(uuid);
423
+ if (frame.state === "completed") {
424
+ inFlight.delete(uuid);
425
+ startedInputs.delete(uuid);
426
+ }
427
+ if (frame.state === "queued" || frame.type === "user") {
428
+ const pending = admissions.get(uuid);
429
+ if (pending) {
430
+ // Once acknowledged, storage owns the admission decision. Its write
431
+ // must not race a native-ack timeout and a second submission.
432
+ clearTimeout(pending.timer);
433
+ try {
434
+ await pending.accept();
435
+ if (inputOwner === pending.receiptOwner && !stopped && !cancelling) acceptedInput = pending.receiptOwner;
436
+ admissions.delete(uuid);
437
+ pending.resolve();
438
+ } catch (error) {
439
+ acceptedInput = null;
440
+ clearFinalAssistantResult();
441
+ pending.reject(error);
442
+ throw error;
443
+ }
444
+ }
445
+ await publish({ type: "admitted", messageId: uuid, userEcho: frame.type === "user" });
446
+ }
447
+ } else if (frame.type === "system") {
448
+ if (active && ["status", "compact_boundary"].includes(frame.subtype)) {
449
+ const nextPhase = frame.subtype === "status" && frame.status === "compacting" ? "compacting" : "";
450
+ // An idle status does not end a wait that only background tasks own.
451
+ if (nextPhase !== phase && (nextPhase || phase !== "background")) {
452
+ phase = nextPhase;
453
+ await publish({ type: "phase", phase });
454
+ }
455
+ }
456
+ if (frame.subtype === "local_command_output" && frame.content) {
457
+ await publishMessage({ id: `claude_${frame.uuid}`, role: "commentary", text: frame.content, complete: true,
458
+ data: { native: { engineId: "claude", type: "system", subtype: "local_command_output" } } });
459
+ }
460
+ const previousTaskCount = tasks.size;
461
+ updateClaudeBackgroundTasks(tasks, frame);
462
+ // A command Claude started before reading a waiting steer would hold it too.
463
+ if (frame.subtype === "task_started") backgroundForegroundWork();
464
+ // Inventory removal can precede the task notification and the main model's
465
+ // response to it. A foreground result written while that worker was live
466
+ // cannot complete this follow-up or retire its admitted application tools.
467
+ if (tasks.size < previousTaskCount) outcome = null;
468
+ if (phase === "background" && !outcome) {
469
+ phase = "";
470
+ await publish({ type: "phase", phase });
471
+ }
472
+ if (["background_tasks_changed", "task_started", "task_notification", "task_updated"].includes(frame.subtype)) {
473
+ if (tasks.size && turnId && !active && !cancelling) {
474
+ generation++;
475
+ active = true;
476
+ clearFinalAssistantResult();
477
+ await publish({ type: "continued", source: frame.subtype });
478
+ }
479
+ await publish({ type: "diagnostic", stage: "background_tasks", nativeEvent: frame.subtype,
480
+ active, pendingCommands: inFlight.size, backgroundTasks: tasks.size });
481
+ }
482
+ } else if (frame.type === "stream_event") {
483
+ const event = frame.event || {};
484
+ if (event.type === "message_start") messageId = event.message?.id || frame.uuid;
485
+ const id = `claude_${messageId}_${event.index}`;
486
+ if (event.type === "content_block_start" && ["text", "thinking"].includes(event.content_block?.type)) {
487
+ messages.set(id, { id, outputId: id, userId: currentCommandId, complete: false, role: event.content_block.type === "thinking" ? "thinking" : "assistant",
488
+ text: event.content_block.text || event.content_block.thinking || "" });
489
+ } else if (event.type === "content_block_delta") {
490
+ const block = messages.get(id);
491
+ const delta = event.delta?.text ?? event.delta?.thinking;
492
+ if (block && typeof delta === "string") {
493
+ block.text += delta;
494
+ checkOutput(block.text, block.role);
495
+ await publishMessage(block);
496
+ await publish({ type: block.role === "thinking" ? "thinking" : "text", text: delta, messageId: id });
497
+ }
498
+ } else if (event.type === "content_block_stop") {
499
+ if (![...pendingAssistantMessages.values()].some(blocks => blocks.some(block => block.outputId === id))) {
500
+ messages.delete(id);
501
+ await publish({ type: "message-complete", messageId: id });
502
+ }
503
+ } else if (event.type === "message_delta" && event.delta?.stop_reason) {
504
+ await flushAssistantMessages(messageId, event.delta.stop_reason === "tool_use" ? "commentary" : "assistant");
505
+ }
506
+ } else if (frame.type === "assistant") {
507
+ const apiMessageId = frame.message?.id;
508
+ if (apiMessageId && frame.message.content?.some(block => block.type === "tool_use")) {
509
+ await flushAssistantMessages(apiMessageId, "commentary");
510
+ }
511
+ for (const block of claudeMessageBlocks(frame)) {
512
+ block.userId = currentCommandId;
513
+ checkOutput(block.text, block.role);
514
+ const live = liveClaudeBlock(messages, messageId, frame, block.role);
515
+ if (live) {
516
+ block.outputId = live.id;
517
+ block.userId = live.userId || block.userId;
518
+ }
519
+ if (apiMessageId && block.role === "assistant") {
520
+ const role = assistantRoles.get(apiMessageId);
521
+ if (role) await publishMessage({ ...block, role });
522
+ else {
523
+ const pending = pendingAssistantMessages.get(apiMessageId) || [];
524
+ pending.push(block);
525
+ pendingAssistantMessages.set(apiMessageId, pending);
526
+ }
527
+ } else await publishMessage(block);
528
+ }
529
+ if (apiMessageId && frame.message.stop_reason) {
530
+ await flushAssistantMessages(apiMessageId, frame.message.stop_reason === "tool_use" ? "commentary" : "assistant");
531
+ }
532
+ } else if (frame.type === "result") {
533
+ const failed = frame.is_error || frame.subtype !== "success";
534
+ const interrupted = ["aborted_streaming", "aborted_tools"].includes(frame.terminal_reason);
535
+ const role = failed || interrupted ? "commentary" : "assistant";
536
+ for (const id of pendingAssistantMessages.keys()) await flushAssistantMessages(id, role);
537
+ const output = typeof frame.structured_output === "object" ? JSON.stringify(frame.structured_output) : String(frame.result || "");
538
+ checkOutput(output);
539
+ const message = !failed ? "" : claudeResultError(frame, output);
540
+ result = failed ? "" : output;
541
+ if (result && ![...messages.values()].some((block) => block.complete && block.role === "assistant" && block.text === result)) {
542
+ await publishMessage(claudeResultMessage(frame, turnId, result));
543
+ }
544
+ inFlight.delete(currentCommandId);
545
+ for (const uuid of startedInputs) inFlight.delete(uuid);
546
+ startedInputs.clear();
547
+ if (cancelling) return;
548
+ outcome = { status: interrupted ? "interrupted" : failed ? "failed" : "completed", error: message };
549
+ // The reply is final, but native background tasks still own the turn
550
+ // (and may continue it). Keep it active and say what it is waiting for.
551
+ if (active && tasks.size && !inFlight.size && phase !== "background") {
552
+ phase = "background";
553
+ await publish({ type: "phase", phase });
554
+ }
555
+ await publish({ type: "diagnostic", stage: "native_result", status: outcome.status,
556
+ terminalReason: frame.terminal_reason, active, pendingCommands: inFlight.size, backgroundTasks: tasks.size });
557
+ }
558
+ if ((frame.type === "system" || frame.type === "result" || frame.type === "command_lifecycle" && frame.state === "completed") &&
559
+ active && !cancelling && outcome && inFlight.size === 0 && tasks.size === 0) {
560
+ active = false;
561
+ phase = "";
562
+ const candidate = publishedAssistant;
563
+ finalAssistantResult = outcome.status === "completed" && !outcome.error && !stopped && !cancelling &&
564
+ admissions.size === 0 && candidate && currentReceiptOwner(candidate.owner) && candidate.text === result
565
+ ? { threadId: conversationId, turnId, inputMessageId: candidate.owner.commandId,
566
+ itemId: candidate.itemId, outputId: candidate.outputId, text: candidate.text,
567
+ conversationTurn: candidate.conversationTurn } : null;
568
+ latestSettlement = { type: "settled", ...read(), generation, source: frame.type };
569
+ try { await publish(latestSettlement); }
570
+ catch (error) { clearFinalAssistantResult(); throw error; }
571
+ }
572
+ }
573
+
574
+ function stop(reason = "Claude stopped before acknowledging the prompt.") {
575
+ generation++;
576
+ stopped = true;
577
+ invalidateFinalAssistantResult();
578
+ for (const pending of admissions.values()) {
579
+ pending.reject(claudeTurnFailure(reason));
580
+ }
581
+ admissions.clear();
582
+ inFlight.clear();
583
+ startedInputs.clear();
584
+ waitingSteers.clear();
585
+ steeringClient = null;
586
+ tasks.clear();
587
+ retainStoppedClaudeMessages(messages, pendingAssistantMessages);
588
+ pendingAssistantMessages.clear();
589
+ assistantRoles.clear();
590
+ if (active) outcome = { status: "interrupted", error: reason };
591
+ active = false;
592
+ phase = "";
593
+ }
594
+
595
+ async function interrupt(client) {
596
+ cancelling = true;
597
+ clearFinalAssistantResult();
598
+ if (client) await client.interrupt();
599
+ // The process host still must drain its owned scope and call stop().
600
+ }
601
+
602
+ function begin({ id, maxOutputCharacters = 4 * 1024 * 1024 } = {}) {
603
+ if (active) throw claudeTurnFailure("This Claude turn is still working.");
604
+ if (!id || !Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) throw new TypeError("Invalid Claude turn configuration.");
605
+ generation++;
606
+ clearTurnView();
607
+ turnId = id;
608
+ maximumOutput = maxOutputCharacters;
609
+ phase = "";
610
+ messageId = "";
611
+ currentCommandId = "";
612
+ cancelling = false;
613
+ stopped = false;
614
+ active = true;
615
+ }
616
+
617
+ function reset() {
618
+ if (active || inFlight.size || tasks.size) throw claudeTurnFailure("Stop Claude before resetting its turn view.");
619
+ clearTurnView();
620
+ }
621
+
622
+ async function send(client, message, { messageId: uuid, accept, timeoutMs = 30_000, steer = false } = {}) {
623
+ if (!active || stopped || cancelling) {
624
+ throw Object.assign(steeringRefusal("Claude has no active turn for this message.", {
625
+ messageCode: "assistant.claude.noActiveTurn", finished: !active && !stopped && !cancelling }), { delivery: "not-sent" });
626
+ }
627
+ if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
628
+ clearFinalAssistantResult();
629
+ acceptedInput = null;
630
+ inputOwner = entry ? { commandId: uuid, process: entry.process, executionId: entry.executionId,
631
+ accountIdentity: entry.accountIdentity } : null;
632
+ const receiptOwner = inputOwner;
633
+ const admitted = Promise.withResolvers();
634
+ void admitted.promise.catch(() => {});
635
+ inFlight.add(uuid);
636
+ if (steer) {
637
+ waitingSteers.add(uuid);
638
+ steeringClient = client;
639
+ }
640
+ const timer = setTimeout(() => admitted.reject(Object.assign(
641
+ claudeTurnFailure("Claude has not acknowledged this prompt. Its delivery is uncertain.", { code: "assistant_claude_admission_unknown" }),
642
+ { delivery: "uncertain" }
643
+ )), timeoutMs);
644
+ admissions.set(uuid, { ...admitted, accept, timer, receiptOwner });
645
+ try {
646
+ await Promise.all([client.send(message, { messageId: uuid, sessionId: conversationId })
647
+ .then(() => { if (steer) backgroundForegroundWork(); }), admitted.promise]);
648
+ } catch (error) {
649
+ waitingSteers.delete(uuid);
650
+ if (inputOwner === receiptOwner) invalidateFinalAssistantResult();
651
+ throw error;
652
+ } finally {
653
+ clearTimeout(timer);
654
+ }
655
+ }
656
+
657
+ async function prepareSteering() {
658
+ if (!active || cancelling) {
659
+ const completed = !active && !stopped && !cancelling && outcome?.status === "completed";
660
+ throw Object.assign(steeringRefusal("This Claude turn cannot accept steering now.", {
661
+ messageCode: "assistant.claude.steeringUnavailable", finished: completed }),
662
+ { delivery: "not-sent" });
663
+ }
664
+ // Claude Code acknowledges streamed input as queued and folds it into the
665
+ // running turn after the current tool call, or starts it once the main
666
+ // loop is idle. Interrupting instead would kill a running foreground tool;
667
+ // send() moves such a tool to the background so the steer is read now.
668
+ // The next input owns completion now: a result or task finishing before
669
+ // it starts must not settle this turn with the previous answer.
670
+ outcome = null;
671
+ acceptedInput = null;
672
+ clearFinalAssistantResult();
673
+ await publish({ type: "diagnostic", stage: "steering_queued", pendingCommands: inFlight.size, backgroundTasks: tasks.size });
674
+ }
675
+
676
+ async function cancel(client, stopProcess) {
677
+ try {
678
+ await interrupt(client);
679
+ } catch {
680
+ // A verified managed-scope stop also handles an unresponsive control pipe.
681
+ } finally {
682
+ // A control acknowledgement is not a process-exit proof. Draining also
683
+ // cancels queued steering prompts and native background tools.
684
+ await stopProcess();
685
+ }
686
+ }
687
+
688
+ return Object.freeze({
689
+ receive,
690
+ read,
691
+ stop,
692
+ interrupt,
693
+ readFinalAssistantResult,
694
+ invalidateFinalAssistantResult,
695
+ ...(entry ? { readResult, updateState, stopProcess } : {}),
696
+ ...(instructions ? {
697
+ prepare: instructions.prepare,
698
+ whenIdle: instructions.whenIdle,
699
+ instructionArguments: instructions.instructionArguments,
700
+ invalidateInstructions: instructions.invalidate
701
+ } : {}),
702
+ begin,
703
+ reset,
704
+ send,
705
+ prepareSteering,
706
+ cancel
707
+ });
708
+ }