clay-server 2.27.0-beta.13 → 2.27.0-beta.14

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.
@@ -0,0 +1,573 @@
1
+ var usersModule = require("./users");
2
+
3
+ function attachMessageProcessor(ctx) {
4
+ var sm = ctx.sm;
5
+ var send = ctx.send;
6
+ var slug = ctx.slug;
7
+ var isMate = ctx.isMate;
8
+ var mateDisplayName = ctx.mateDisplayName;
9
+ var pushModule = ctx.pushModule;
10
+ var getSDK = ctx.getSDK;
11
+ var onProcessingChanged = ctx.onProcessingChanged;
12
+ var onTurnDone = ctx.onTurnDone;
13
+ var opts = ctx.opts;
14
+ var discoverSkillDirs = ctx.discoverSkillDirs;
15
+ var mergeSkills = ctx.mergeSkills;
16
+
17
+ function sendAndRecord(session, obj) {
18
+ sm.sendAndRecord(session, obj);
19
+ }
20
+
21
+ function sendToSession(session, obj) {
22
+ sm.sendToSession(session, obj);
23
+ }
24
+
25
+ function toolActivityTextForSubagent(name, input) {
26
+ if (name === "Bash" && input && input.description) return input.description;
27
+ if (name === "Read" && input && input.file_path) return "Reading " + input.file_path.split("/").pop();
28
+ if (name === "Edit" && input && input.file_path) return "Editing " + input.file_path.split("/").pop();
29
+ if (name === "Write" && input && input.file_path) return "Writing " + input.file_path.split("/").pop();
30
+ if (name === "Grep" && input && input.pattern) return "Searching for " + input.pattern;
31
+ if (name === "Glob" && input && input.pattern) return "Finding " + input.pattern;
32
+ if (name === "WebSearch" && input && input.query) return "Searching: " + input.query;
33
+ if (name === "WebFetch") return "Fetching URL...";
34
+ if (name === "Task" && input && input.description) return input.description;
35
+ return "Running " + name + "...";
36
+ }
37
+
38
+ function processSubagentMessage(session, parsed) {
39
+ var parentId = parsed.parent_tool_use_id;
40
+ var content = parsed.message.content;
41
+ if (!Array.isArray(content)) return;
42
+
43
+ if (parsed.type === "assistant") {
44
+ // Extract tool_use blocks from sub-agent assistant messages
45
+ for (var i = 0; i < content.length; i++) {
46
+ var block = content[i];
47
+ if (block.type === "tool_use") {
48
+ var activityText = toolActivityTextForSubagent(block.name, block.input);
49
+ sendAndRecord(session, {
50
+ type: "subagent_tool",
51
+ parentToolId: parentId,
52
+ toolName: block.name,
53
+ toolId: block.id,
54
+ text: activityText,
55
+ });
56
+ } else if (block.type === "thinking") {
57
+ sendAndRecord(session, {
58
+ type: "subagent_activity",
59
+ parentToolId: parentId,
60
+ text: "Thinking...",
61
+ });
62
+ } else if (block.type === "text" && block.text) {
63
+ sendAndRecord(session, {
64
+ type: "subagent_activity",
65
+ parentToolId: parentId,
66
+ text: "Writing response...",
67
+ });
68
+ }
69
+ }
70
+ }
71
+ // user messages with parent_tool_use_id contain tool_results -- skip silently
72
+ }
73
+
74
+ function processSDKMessage(session, parsed) {
75
+ // Timing: log key SDK milestones relative to query start
76
+ if (session._queryStartTs) {
77
+ var _elapsed = Date.now() - session._queryStartTs;
78
+ if (parsed.type === "system" && parsed.subtype === "init") {
79
+ console.log("[PERF] processSDKMessage: system/init +" + _elapsed + "ms");
80
+ }
81
+ if (parsed.type === "stream_event" && parsed.event) {
82
+ if (parsed.event.type === "message_start") {
83
+ console.log("[PERF] processSDKMessage: message_start (API response begun) +" + _elapsed + "ms");
84
+ }
85
+ if (parsed.event.type === "content_block_delta" && !session._firstTextLogged) {
86
+ session._firstTextLogged = true;
87
+ console.log("[PERF] processSDKMessage: FIRST content_block_delta (visible text) +" + _elapsed + "ms");
88
+ }
89
+ }
90
+ if (parsed.type === "result") {
91
+ console.log("[PERF] processSDKMessage: result +" + _elapsed + "ms");
92
+ }
93
+ }
94
+
95
+ // Extract session_id from any message that carries it
96
+ if (parsed.session_id && !session.cliSessionId) {
97
+ session.cliSessionId = parsed.session_id;
98
+ sm.saveSessionFile(session);
99
+ sendAndRecord(session, { type: "session_id", cliSessionId: session.cliSessionId });
100
+ } else if (parsed.session_id) {
101
+ session.cliSessionId = parsed.session_id;
102
+ }
103
+
104
+ // Capture message UUIDs for rewind support
105
+ if (parsed.uuid) {
106
+ if (parsed.type === "user" && !parsed.parent_tool_use_id) {
107
+ session.messageUUIDs.push({ uuid: parsed.uuid, type: "user", historyIndex: session.history.length });
108
+ sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "user" });
109
+ } else if (parsed.type === "assistant") {
110
+ session.messageUUIDs.push({ uuid: parsed.uuid, type: "assistant", historyIndex: session.history.length });
111
+ sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "assistant" });
112
+ }
113
+ }
114
+
115
+ // Cache slash_commands and model from CLI init message
116
+ if (parsed.type === "system" && parsed.subtype === "init") {
117
+ var fsSkills = discoverSkillDirs();
118
+ sm.skillNames = mergeSkills(parsed.skills, fsSkills);
119
+ if (parsed.slash_commands) {
120
+ // Union: SDK slash_commands + merged skills (deduplicated)
121
+ var seen = new Set();
122
+ var combined = [];
123
+ var all = parsed.slash_commands.concat(Array.from(sm.skillNames));
124
+ for (var k = 0; k < all.length; k++) {
125
+ if (!seen.has(all[k])) {
126
+ seen.add(all[k]);
127
+ combined.push(all[k]);
128
+ }
129
+ }
130
+ sm.slashCommands = combined;
131
+ send({ type: "slash_commands", commands: sm.slashCommands });
132
+ }
133
+ if (parsed.model) {
134
+ sm.currentModel = sm._savedDefaultModel || parsed.model;
135
+ send({ type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
136
+ }
137
+ if (parsed.fast_mode_state) {
138
+ sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
139
+ }
140
+ }
141
+
142
+ if (parsed.type === "stream_event" && parsed.event) {
143
+ var evt = parsed.event;
144
+
145
+ if (evt.type === "message_start" && evt.message && evt.message.usage) {
146
+ var u = evt.message.usage;
147
+ session.lastStreamInputTokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0);
148
+ }
149
+
150
+ if (evt.type === "content_block_start") {
151
+ var block = evt.content_block;
152
+ var idx = evt.index;
153
+
154
+ if (block.type === "tool_use") {
155
+ session.blocks[idx] = { type: "tool_use", id: block.id, name: block.name, inputJson: "" };
156
+ sendAndRecord(session, { type: "tool_start", id: block.id, name: block.name });
157
+ } else if (block.type === "thinking") {
158
+ session.blocks[idx] = { type: "thinking", thinkingText: "", startTime: Date.now() };
159
+ sendAndRecord(session, { type: "thinking_start" });
160
+ } else if (block.type === "text") {
161
+ session.blocks[idx] = { type: "text" };
162
+ }
163
+ }
164
+
165
+ if (evt.type === "content_block_delta" && evt.delta) {
166
+ var idx = evt.index;
167
+
168
+ if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
169
+ session.streamedText = true;
170
+ if (session.responsePreview.length < 200) {
171
+ session.responsePreview += evt.delta.text;
172
+ }
173
+ // Accumulate text for mate DM response
174
+ if (typeof session._mateDmResponseText === "string") {
175
+ session._mateDmResponseText += evt.delta.text;
176
+ }
177
+ sendAndRecord(session, { type: "delta", text: evt.delta.text });
178
+ } else if (evt.delta.type === "input_json_delta" && session.blocks[idx]) {
179
+ session.blocks[idx].inputJson += evt.delta.partial_json;
180
+ } else if (evt.delta.type === "thinking_delta" && session.blocks[idx]) {
181
+ session.blocks[idx].thinkingText += evt.delta.thinking;
182
+ sendAndRecord(session, { type: "thinking_delta", text: evt.delta.thinking });
183
+ }
184
+ }
185
+
186
+ if (evt.type === "content_block_stop") {
187
+ var idx = evt.index;
188
+ var block = session.blocks[idx];
189
+
190
+ if (block && block.type === "tool_use") {
191
+ var input = {};
192
+ try { input = JSON.parse(block.inputJson); } catch (e) {}
193
+ sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
194
+
195
+ // Track active Task tools for sub-agent done detection
196
+ if (block.name === "Task") {
197
+ if (!session.activeTaskToolIds) session.activeTaskToolIds = {};
198
+ session.activeTaskToolIds[block.id] = true;
199
+ }
200
+
201
+ if (pushModule && block.name === "AskUserQuestion" && input.questions) {
202
+ var q = input.questions[0];
203
+ pushModule.sendPush({
204
+ type: "ask_user",
205
+ slug: slug,
206
+ title: (mateDisplayName || "Claude") + " has a question",
207
+ body: q ? q.question : "Waiting for your response",
208
+ tag: "claude-ask",
209
+ });
210
+ }
211
+ } else if (block && block.type === "thinking") {
212
+ var duration = block.startTime ? (Date.now() - block.startTime) / 1000 : 0;
213
+ sendAndRecord(session, { type: "thinking_stop", duration: duration });
214
+ }
215
+
216
+ delete session.blocks[idx];
217
+ }
218
+
219
+ } else if ((parsed.type === "assistant" || parsed.type === "user") && parsed.message && parsed.message.content) {
220
+ // Sub-agent messages: extract tool_use blocks for activity display
221
+ if (parsed.parent_tool_use_id) {
222
+ processSubagentMessage(session, parsed);
223
+ return;
224
+ }
225
+
226
+ var content = parsed.message.content;
227
+
228
+ // Fallback: if assistant text wasn't streamed via deltas, send it now
229
+ if (parsed.type === "assistant" && !session.streamedText && Array.isArray(content)) {
230
+ var assistantText = content
231
+ .filter(function(c) { return c.type === "text"; })
232
+ .map(function(c) { return c.text; })
233
+ .join("");
234
+ if (assistantText) {
235
+ if (session.responsePreview.length < 200) {
236
+ session.responsePreview += assistantText;
237
+ }
238
+ sendAndRecord(session, { type: "delta", text: assistantText });
239
+ }
240
+ }
241
+
242
+ // Check for local slash command output in user messages
243
+ if (parsed.type === "user") {
244
+ var fullText = "";
245
+ if (typeof content === "string") {
246
+ fullText = content;
247
+ } else if (Array.isArray(content)) {
248
+ fullText = content
249
+ .filter(function(c) { return c.type === "text"; })
250
+ .map(function(c) { return c.text; })
251
+ .join("\n");
252
+ }
253
+ if (fullText.indexOf("local-command-stdout") !== -1) {
254
+ var m = fullText.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
255
+ if (m) {
256
+ sendAndRecord(session, { type: "slash_command_result", text: m[1].trim() });
257
+ }
258
+ }
259
+ }
260
+
261
+ if (Array.isArray(content)) {
262
+ for (var i = 0; i < content.length; i++) {
263
+ var block = content[i];
264
+ if (block.type === "tool_result" && !session.sentToolResults[block.tool_use_id]) {
265
+ // Clear active Task tool when its result arrives
266
+ if (session.activeTaskToolIds && session.activeTaskToolIds[block.tool_use_id]) {
267
+ sendAndRecord(session, {
268
+ type: "subagent_done",
269
+ parentToolId: block.tool_use_id,
270
+ });
271
+ delete session.activeTaskToolIds[block.tool_use_id];
272
+ }
273
+ var resultText = "";
274
+ var resultImages = [];
275
+ if (typeof block.content === "string") {
276
+ resultText = block.content;
277
+ } else if (Array.isArray(block.content)) {
278
+ resultText = block.content
279
+ .filter(function(c) { return c.type === "text"; })
280
+ .map(function(c) { return c.text; })
281
+ .join("\n");
282
+ for (var ri = 0; ri < block.content.length; ri++) {
283
+ var rc = block.content[ri];
284
+ if (rc.type === "image" && rc.source) {
285
+ resultImages.push({
286
+ mediaType: rc.source.media_type,
287
+ data: rc.source.data,
288
+ });
289
+ }
290
+ }
291
+ }
292
+ session.sentToolResults[block.tool_use_id] = true;
293
+ var toolResultMsg = {
294
+ type: "tool_result",
295
+ id: block.tool_use_id,
296
+ content: resultText,
297
+ is_error: block.is_error || false,
298
+ };
299
+ if (resultImages.length > 0) toolResultMsg.images = resultImages;
300
+ sendAndRecord(session, toolResultMsg);
301
+ }
302
+ }
303
+ }
304
+
305
+ } else if (parsed.type === "result") {
306
+ session.blocks = {};
307
+ session.sentToolResults = {};
308
+ session.pendingPermissions = {};
309
+ session.pendingElicitations = {};
310
+ // Record ask_user_answered for any leftover pending questions so replay pairs correctly
311
+ var leftoverAskIds = Object.keys(session.pendingAskUser);
312
+ for (var lai = 0; lai < leftoverAskIds.length; lai++) {
313
+ sendAndRecord(session, { type: "ask_user_answered", toolId: leftoverAskIds[lai] });
314
+ }
315
+ session.pendingAskUser = {};
316
+ session.activeTaskToolIds = {};
317
+ session.taskIdMap = {};
318
+ // Only clear rateLimitResetsAt on genuine success (non-zero cost).
319
+ // When rate-limited, the SDK sends result with zero cost right after
320
+ // rate_limit_event; clearing here would prevent auto-continue scheduling.
321
+ if (parsed.total_cost_usd && parsed.total_cost_usd > 0) {
322
+ session.rateLimitResetsAt = null;
323
+ }
324
+ console.log("[sdk-bridge] result handler: session " + session.localId + " cost=" + parsed.total_cost_usd + " rateLimitResetsAt=" + session.rateLimitResetsAt);
325
+
326
+ // Handle SDK execution errors: show the error to the user instead of
327
+ // silently swallowing it. These have subtype "error_during_execution".
328
+ if (parsed.subtype === "error_during_execution") {
329
+ var execErrors = parsed.errors || [];
330
+ var execError = execErrors.length > 0
331
+ ? execErrors.join("; ")
332
+ : "Unknown SDK error";
333
+ if (parsed.terminal_reason) execError += " (reason: " + parsed.terminal_reason + ")";
334
+ console.error("[sdk-bridge] Execution error for session " + session.localId + ": " + execError);
335
+ session.isProcessing = false;
336
+ onProcessingChanged();
337
+ sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
338
+ sendAndRecord(session, { type: "done", code: 1 });
339
+ sm.broadcastSessionList();
340
+ return;
341
+ }
342
+
343
+ session.isProcessing = false;
344
+ onProcessingChanged();
345
+ // Detect "Not logged in" scenario early for the check below
346
+ var previewTrimmed = (session.responsePreview || "").trim();
347
+ var isZeroCost = !parsed.total_cost_usd || parsed.total_cost_usd === 0;
348
+ var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
349
+ && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
350
+ // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
351
+ if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
352
+ session.queryInstance.getContextUsage().then(function(ctxUsage) {
353
+ session.lastContextUsage = ctxUsage;
354
+ sendToSession(session, { type: "context_usage", data: ctxUsage });
355
+ }).catch(function(e) {
356
+ console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
357
+ });
358
+ }
359
+ var lastStreamInput = session.lastStreamInputTokens || null;
360
+ session.lastStreamInputTokens = null;
361
+ sendAndRecord(session, {
362
+ type: "result",
363
+ cost: parsed.total_cost_usd,
364
+ duration: parsed.duration_ms,
365
+ usage: parsed.usage || null,
366
+ modelUsage: parsed.modelUsage || null,
367
+ sessionId: parsed.session_id,
368
+ lastStreamInputTokens: lastStreamInput,
369
+ });
370
+ if (parsed.fast_mode_state) {
371
+ sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
372
+ }
373
+ // Detect "Not logged in / Please run /login" from SDK.
374
+ // This is a short canned response with zero cost, not actual AI output.
375
+ if (isLoginPrompt) {
376
+ var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
377
+ var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
378
+ var canAutoLogin = !usersModule.isMultiUser()
379
+ || !!authLinuxUser
380
+ || (authUser && authUser.role === "admin");
381
+ sendAndRecord(session, {
382
+ type: "auth_required",
383
+ text: "Claude Code is not logged in.",
384
+ linuxUser: authLinuxUser,
385
+ canAutoLogin: canAutoLogin,
386
+ });
387
+ // Reset CLI session so next query starts fresh with new auth
388
+ session.cliSessionId = null;
389
+ }
390
+ sendAndRecord(session, { type: "done", code: 0 });
391
+ if (pushModule) {
392
+ var preview = (session.responsePreview || "").replace(/\s+/g, " ").trim();
393
+ if (preview.length > 140) preview = preview.substring(0, 140) + "...";
394
+ pushModule.sendPush({
395
+ type: "done",
396
+ slug: slug,
397
+ title: mateDisplayName ? (mateDisplayName + " responded") : (session.title || "Claude"),
398
+ body: preview || "Response ready",
399
+ tag: "claude-done",
400
+ });
401
+ }
402
+ // Reset for next turn in the same query
403
+ session.lastActivityAt = Date.now();
404
+ var donePreview = session.responsePreview || "";
405
+ session.responsePreview = "";
406
+ session.streamedText = false;
407
+ sm.broadcastSessionList();
408
+ if (onTurnDone) {
409
+ try { onTurnDone(session, donePreview); } catch (e) {}
410
+ }
411
+
412
+ } else if (parsed.type === "system" && parsed.subtype === "status") {
413
+ if (parsed.status === "compacting") {
414
+ sendAndRecord(session, { type: "compacting", active: true });
415
+ } else if (session.compacting) {
416
+ sendAndRecord(session, { type: "compacting", active: false });
417
+ }
418
+ session.compacting = parsed.status === "compacting";
419
+
420
+ } else if (parsed.type === "system" && parsed.subtype === "task_started") {
421
+ var parentId = parsed.tool_use_id;
422
+ if (parentId) {
423
+ if (!session.taskIdMap) session.taskIdMap = {};
424
+ session.taskIdMap[parentId] = parsed.task_id;
425
+ sendAndRecord(session, {
426
+ type: "task_started",
427
+ parentToolId: parentId,
428
+ taskId: parsed.task_id,
429
+ description: parsed.description || "",
430
+ });
431
+ }
432
+
433
+ } else if (parsed.type === "system" && parsed.subtype === "task_progress") {
434
+ var parentId = parsed.tool_use_id;
435
+ if (parentId) {
436
+ sendAndRecord(session, {
437
+ type: "task_progress",
438
+ parentToolId: parentId,
439
+ taskId: parsed.task_id,
440
+ usage: parsed.usage || null,
441
+ lastToolName: parsed.last_tool_name || null,
442
+ description: parsed.description || "",
443
+ summary: parsed.summary || null,
444
+ });
445
+ }
446
+
447
+ } else if (parsed.type === "tool_progress") {
448
+ // Sub-agent tool_progress: forward as activity update
449
+ var parentId = parsed.parent_tool_use_id;
450
+ if (parentId) {
451
+ sendAndRecord(session, {
452
+ type: "subagent_activity",
453
+ parentToolId: parentId,
454
+ text: parsed.content || "",
455
+ });
456
+ }
457
+
458
+ } else if (parsed.type === "task_notification") {
459
+ var parentId = parsed.parent_tool_use_id;
460
+ if (parentId) {
461
+ sendAndRecord(session, {
462
+ type: "subagent_done",
463
+ parentToolId: parentId,
464
+ status: parsed.status || "completed",
465
+ summary: parsed.summary || "",
466
+ usage: parsed.usage || null,
467
+ });
468
+ }
469
+ if (session.taskIdMap) {
470
+ for (var k in session.taskIdMap) {
471
+ if (session.taskIdMap[k] === parsed.task_id) {
472
+ delete session.taskIdMap[k];
473
+ break;
474
+ }
475
+ }
476
+ }
477
+
478
+ } else if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
479
+ var info = parsed.rate_limit_info;
480
+ console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
481
+
482
+ // Broadcast reset time for top-bar usage link
483
+ if (info.rateLimitType && info.resetsAt) {
484
+ send({
485
+ type: "rate_limit_usage",
486
+ rateLimitType: info.rateLimitType,
487
+ resetsAt: info.resetsAt * 1000,
488
+ status: info.status,
489
+ });
490
+ }
491
+
492
+ // Warning/rejection handling (existing behavior)
493
+ if (info.status === "allowed_warning" || info.status === "rejected") {
494
+ sendAndRecord(session, {
495
+ type: "rate_limit",
496
+ status: info.status,
497
+ resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
498
+ rateLimitType: info.rateLimitType || null,
499
+ utilization: info.utilization || null,
500
+ isUsingOverage: info.isUsingOverage || false,
501
+ });
502
+ // Track rejection for auto-continue / scheduled message support
503
+ if (info.status === "rejected" && info.resetsAt) {
504
+ session.rateLimitResetsAt = info.resetsAt * 1000;
505
+
506
+ // Schedule auto-continue immediately on rejection (don't wait for
507
+ // query completion which has timing issues with worker/non-worker paths).
508
+ if (!session.scheduledMessage && !session.destroying) {
509
+ var acEnabled = session.onQueryComplete ||
510
+ (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
511
+ console.log("[sdk-bridge] rate_limit rejected: acEnabled=" + acEnabled + " overage=" + !!info.isUsingOverage + " session=" + session.localId);
512
+ if (acEnabled) {
513
+ session.rateLimitAutoContinuePending = true;
514
+ if (info.isUsingOverage) {
515
+ // Extra usage available: send continue immediately (5s delay for query to finish)
516
+ console.log("[sdk-bridge] Overage available, sending immediate continue for session " + session.localId);
517
+ session.rateLimitResetsAt = null;
518
+ if (typeof opts.scheduleMessage === "function") {
519
+ opts.scheduleMessage(session, "continue", Date.now());
520
+ }
521
+ } else {
522
+ // No overage: schedule after rate limit resets
523
+ var acResetsAt = session.rateLimitResetsAt;
524
+ session.rateLimitResetsAt = null;
525
+ console.log("[sdk-bridge] Scheduling auto-continue on rate limit rejection for session " + session.localId);
526
+ if (typeof opts.scheduleMessage === "function") {
527
+ opts.scheduleMessage(session, "continue", acResetsAt);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+
535
+ } else if (parsed.type === "prompt_suggestion") {
536
+ sendAndRecord(session, {
537
+ type: "prompt_suggestion",
538
+ suggestion: parsed.suggestion || "",
539
+ });
540
+
541
+ } else if (parsed.type === "system" && parsed.subtype === "api_retry") {
542
+ // Transient retry notification — show in UI but don't persist in history
543
+ var retryText = parsed.message || parsed.error || "Retrying API request...";
544
+ sendToSession(session, { type: "system_info", text: retryText });
545
+
546
+ } else if (parsed.type === "system") {
547
+ // Catch-all for unhandled system subtypes (e.g. hook-block errors).
548
+ // Extract any error text and surface it in the UI.
549
+ var sysText = parsed.error || parsed.message || parsed.text || "";
550
+ if (!sysText && Array.isArray(parsed.content)) {
551
+ sysText = parsed.content
552
+ .filter(function(c) { return c.type === "text"; })
553
+ .map(function(c) { return c.text; })
554
+ .join("\n");
555
+ }
556
+ if (sysText) {
557
+ console.log("[sdk-bridge] Unhandled system message (subtype=" + (parsed.subtype || "none") + "): " + sysText.substring(0, 200));
558
+ sendAndRecord(session, { type: "error", text: sysText });
559
+ }
560
+ } else if (parsed.type && parsed.type !== "user") {
561
+ }
562
+ }
563
+
564
+ return {
565
+ processSDKMessage: processSDKMessage,
566
+ sendAndRecord: sendAndRecord,
567
+ sendToSession: sendToSession,
568
+ processSubagentMessage: processSubagentMessage,
569
+ toolActivityTextForSubagent: toolActivityTextForSubagent,
570
+ };
571
+ }
572
+
573
+ module.exports = { attachMessageProcessor: attachMessageProcessor };
@@ -0,0 +1,42 @@
1
+ // Async message queue for streaming input to SDK
2
+ function createMessageQueue() {
3
+ var queue = [];
4
+ var waiting = null;
5
+ var ended = false;
6
+ return {
7
+ push: function(msg) {
8
+ if (waiting) {
9
+ var resolve = waiting;
10
+ waiting = null;
11
+ resolve({ value: msg, done: false });
12
+ } else {
13
+ queue.push(msg);
14
+ }
15
+ },
16
+ end: function() {
17
+ ended = true;
18
+ if (waiting) {
19
+ var resolve = waiting;
20
+ waiting = null;
21
+ resolve({ value: undefined, done: true });
22
+ }
23
+ },
24
+ [Symbol.asyncIterator]: function() {
25
+ return {
26
+ next: function() {
27
+ if (queue.length > 0) {
28
+ return Promise.resolve({ value: queue.shift(), done: false });
29
+ }
30
+ if (ended) {
31
+ return Promise.resolve({ value: undefined, done: true });
32
+ }
33
+ return new Promise(function(resolve) {
34
+ waiting = resolve;
35
+ });
36
+ },
37
+ };
38
+ },
39
+ };
40
+ }
41
+
42
+ module.exports = { createMessageQueue: createMessageQueue };