whale-code 6.4.0 → 6.5.0

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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -13,6 +13,7 @@ import { processStreamWithCallbacks } from "../../shared/sse-parser.js";
13
13
  import { MODELS } from "../../shared/constants.js";
14
14
  import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch.js";
15
15
  import { getCachedToolDefs, getFullToolSchemas } from "../tool-router.js";
16
+ import { queueSpan, auditRowToSpan } from "./clickhouse-buffer.js";
16
17
  import { DELEGATE_TASK_TOOL_DEF, runServerSubagent, } from "./server-subagent.js";
17
18
  import { handleTranscribe } from "../handlers/transcription.js";
18
19
  import { preCompact } from "./compaction-service.js";
@@ -49,7 +50,7 @@ function mapToolChoiceForAnthropic(tc) {
49
50
  // UNIFIED AGENT LOOP
50
51
  // ============================================================================
51
52
  export async function runServerAgentLoop(opts) {
52
- const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd = DEFAULT_SESSION_COST_BUDGET_USD, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 5 * 60 * 1000, } = opts;
53
+ const { anthropic, model, systemPrompt, messages, tools: inputTools, maxTurns, temperature, enableDelegation = true, enablePromptCaching = true, enableStreaming = true, maxConcurrentTools = DEFAULT_MAX_CONCURRENT_TOOLS, maxCostUsd = DEFAULT_SESSION_COST_BUDGET_USD, onText, onToolStart, onCitation, documents, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
53
54
  // Auto-inject delegate_task for all models (subagents always use Claude Haiku/Sonnet)
54
55
  // activeTools is mutable — discover_tools adds to it during the session
55
56
  const activeTools = [...inputTools];
@@ -92,10 +93,12 @@ export async function runServerAgentLoop(opts) {
92
93
  let sessionCostUsd = 0;
93
94
  let compactionCount = 0;
94
95
  let finalResponse = "";
96
+ let lastStopReason = "end_turn";
95
97
  const allTextResponses = [];
96
98
  const allToolNames = [];
97
99
  const allCitations = [];
98
100
  const turnMetrics = [];
101
+ const costWarningsEmitted = new Set();
99
102
  while (turnCount < maxTurns) {
100
103
  // Abort checks
101
104
  if (clientDisconnected.value) {
@@ -164,13 +167,17 @@ export async function runServerAgentLoop(opts) {
164
167
  ];
165
168
  // Resolve tool_choice for this turn
166
169
  const recentToolUses = turnMetrics.slice(-3).flatMap(t => t.toolsUsed);
167
- const resolvedToolChoice = resolveToolChoice({
170
+ let resolvedToolChoice = resolveToolChoice({
168
171
  toolChoice: opts.toolChoice,
169
172
  turnCount,
170
173
  recentToolUses,
171
174
  availableToolNames: tools.map(t => t.name),
172
175
  userMessage: firstUserText,
173
176
  });
177
+ // Anthropic API: forced tool_choice ("any" or specific tool) is incompatible with thinking — downgrade to "auto"
178
+ if (thinkingCfg.thinking.type !== "disabled" && resolvedToolChoice !== "auto" && resolvedToolChoice !== "none") {
179
+ resolvedToolChoice = "auto";
180
+ }
174
181
  const { toolChoice: anthropicToolChoice, omitTools } = mapToolChoiceForAnthropic(resolvedToolChoice);
175
182
  if (omitTools) {
176
183
  log.info({ turn: turnCount, resolvedToolChoice }, "tool_choice=none — omitting tools");
@@ -254,6 +261,15 @@ export async function runServerAgentLoop(opts) {
254
261
  cacheReadTokens += turnCacheRead;
255
262
  // Update cost (include cache tokens for accurate pricing)
256
263
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
264
+ // Graduated cost warnings — give the LLM visibility into spend
265
+ if (isFinite(maxCostUsd)) {
266
+ for (const pct of [25, 50, 75]) {
267
+ if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
268
+ costWarningsEmitted.add(pct);
269
+ onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
270
+ }
271
+ }
272
+ }
257
273
  // Record per-turn metrics for observability
258
274
  const turnToolNames = toolUseBlocks.map(b => b.name);
259
275
  turnMetrics.push({
@@ -271,6 +287,7 @@ export async function runServerAgentLoop(opts) {
271
287
  // Compaction handling — API paused after generating summary.
272
288
  // Preserve last 2 messages (1 user + 1 assistant turn) for continuity,
273
289
  // then resume. This is NOT a new turn — just context compression.
290
+ lastStopReason = streamResult.stopReason || "end_turn";
274
291
  if (streamResult.stopReason === "compaction" && compactionContent) {
275
292
  compactionCount++;
276
293
  log.info({ compactionCount }, "compaction — preserving last 2 messages, resuming");
@@ -331,6 +348,15 @@ export async function runServerAgentLoop(opts) {
331
348
  totalIn += subagentTokens.input;
332
349
  totalOut += subagentTokens.output;
333
350
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + subagentTokens.costUsd;
351
+ // Cost warnings after subagent aggregation (subagents can be expensive)
352
+ if (isFinite(maxCostUsd)) {
353
+ for (const pct of [25, 50, 75]) {
354
+ if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
355
+ costWarningsEmitted.add(pct);
356
+ onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
357
+ }
358
+ }
359
+ }
334
360
  const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks, compactionContent });
335
361
  messages.push({ role: "assistant", content: assistantContent });
336
362
  messages.push({ role: "user", content: toolResults });
@@ -426,6 +452,15 @@ export async function runServerAgentLoop(opts) {
426
452
  }
427
453
  }
428
454
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens);
455
+ // Graduated cost warnings (non-streaming path)
456
+ if (isFinite(maxCostUsd)) {
457
+ for (const pct of [25, 50, 75]) {
458
+ if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
459
+ costWarningsEmitted.add(pct);
460
+ onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
461
+ }
462
+ }
463
+ }
429
464
  // Record per-turn metrics (non-streaming)
430
465
  const nsTurnToolNames = toolUseBlocks.map(b => b.name);
431
466
  turnMetrics.push({
@@ -440,6 +475,7 @@ export async function runServerAgentLoop(opts) {
440
475
  });
441
476
  if (currentText)
442
477
  allTextResponses.push(currentText);
478
+ lastStopReason = response.stop_reason || "end_turn";
443
479
  // Compaction handling (non-streaming) — same logic as streaming path
444
480
  if (response.stop_reason === "compaction" && nsCompactionContent !== null) {
445
481
  compactionCount++;
@@ -488,6 +524,15 @@ export async function runServerAgentLoop(opts) {
488
524
  totalIn += nonStreamSubTokens.input;
489
525
  totalOut += nonStreamSubTokens.output;
490
526
  sessionCostUsd = estimateCostUsd(totalIn, totalOut, model, 0, cacheReadTokens, cacheCreationTokens) + nonStreamSubTokens.costUsd;
527
+ // Cost warnings after subagent aggregation (non-streaming)
528
+ if (isFinite(maxCostUsd)) {
529
+ for (const pct of [25, 50, 75]) {
530
+ if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
531
+ costWarningsEmitted.add(pct);
532
+ onText?.(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`);
533
+ }
534
+ }
535
+ }
491
536
  const assistantContent = buildAssistantContent({ text: currentText, toolUseBlocks });
492
537
  messages.push({ role: "assistant", content: assistantContent });
493
538
  messages.push({ role: "user", content: toolResults });
@@ -514,21 +559,22 @@ export async function runServerAgentLoop(opts) {
514
559
  loopDetectorStats: loopDetector.getSessionStats(),
515
560
  turns: turnMetrics,
516
561
  citations: allCitations,
562
+ stopReason: lastStopReason,
517
563
  };
518
564
  }
519
565
  // ============================================================================
520
566
  // TOOL EXECUTOR FACTORY — creates executor for dispatchTools with delegation
521
567
  // ============================================================================
522
568
  function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredToolNames) {
523
- const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 5 * 60 * 1000, } = opts;
569
+ const { anthropic, supabase, storeId, traceId, userId, userEmail, conversationId, agentId, executeTool, onToolResult, onToolProgress, onSubagentProgress, clientDisconnected = { value: false }, startedAt = Date.now(), maxDurationMs = 15 * 60 * 1000, } = opts;
524
570
  return async (name, input) => {
525
571
  allToolNames.push(name);
526
- // Subagent delegation
572
+ // Subagent delegation — demote models to control cost (sub-agents should never run Opus)
527
573
  if (name === "delegate_task") {
528
574
  const subPrompt = String(input.prompt || "");
529
575
  const subModelInput = String(input.model || "haiku");
530
- const subModel = (subModelInput === "opus" ? "opus" :
531
- subModelInput === "sonnet" ? "sonnet" : "haiku");
576
+ const subModel = (subModelInput === "opus" ? "sonnet" :
577
+ subModelInput === "sonnet" ? "haiku" : "haiku");
532
578
  const subMaxTurns = Math.min(Math.max(1, Number(input.max_turns) || 6), 12);
533
579
  const subTools = tools.filter((t) => t.name !== "delegate_task");
534
580
  const subId = `sub-${Date.now().toString(36)}`;
@@ -547,37 +593,37 @@ function makeToolExecutor(opts, tools, allToolNames, subagentTokens, discoveredT
547
593
  : subModel === "sonnet" ? MODELS.SONNET : MODELS.HAIKU;
548
594
  try {
549
595
  const subEndTime = Date.now();
550
- const subBytes = new Uint8Array(8);
551
- crypto.getRandomValues(subBytes);
552
- const subSpanId = Array.from(subBytes).map(b => b.toString(16).padStart(2, "0")).join("");
553
- await supabase.from("audit_logs").insert({
596
+ queueSpan(auditRowToSpan({
554
597
  action: "chat.subagent_complete", severity: "info",
555
598
  store_id: storeId || null, resource_type: "chat_subagent",
556
599
  resource_id: agentId || null, request_id: traceId || null,
557
600
  conversation_id: conversationId || null, source: "server_subagent",
558
- user_id: userId || null, user_email: userEmail || null,
601
+ user_id: userId || null,
602
+ user_email: userEmail || null,
559
603
  input_tokens: subResult.tokensUsed.input, output_tokens: subResult.tokensUsed.output,
560
604
  total_cost: subResult.costUsd, model: subModelId, duration_ms: subDurationMs,
561
- // OTEL fields
562
605
  trace_id: traceId || null,
563
- span_id: subSpanId,
564
606
  span_kind: "INTERNAL",
565
607
  service_name: "agent-server",
566
608
  status_code: subResult.success ? "OK" : "ERROR",
567
609
  start_time: new Date(subEndTime - subDurationMs).toISOString(),
568
610
  end_time: new Date(subEndTime).toISOString(),
611
+ stop_reason: subResult.stopReason || undefined,
612
+ turn_number: subResult.turnCount || 1,
613
+ parent_conversation_id: conversationId || undefined,
569
614
  details: {
570
615
  subagent_model: subModel, turn_count: subResult.turnCount,
571
616
  tool_calls: subResult.toolsUsed.length, tool_names: subResult.toolsUsed,
572
617
  cost_usd: subResult.costUsd, success: subResult.success,
573
618
  prompt_preview: subPrompt.substring(0, 200),
574
- // gen_ai fields for SwiftUI cost display
575
619
  "gen_ai.request.model": subModelId,
576
620
  "gen_ai.usage.input_tokens": subResult.tokensUsed.input,
577
621
  "gen_ai.usage.output_tokens": subResult.tokensUsed.output,
622
+ "gen_ai.usage.cache_read_tokens": subResult.tokensUsed.cacheRead || 0,
623
+ "gen_ai.usage.cache_creation_tokens": subResult.tokensUsed.cacheCreation || 0,
578
624
  "gen_ai.usage.cost": subResult.costUsd,
579
625
  },
580
- });
626
+ }));
581
627
  }
582
628
  catch (err) {
583
629
  log.error({ err: err.message }, "failed to log subagent delegation audit");
@@ -12,10 +12,13 @@ export interface SubagentResult {
12
12
  tokensUsed: {
13
13
  input: number;
14
14
  output: number;
15
+ cacheRead: number;
16
+ cacheCreation: number;
15
17
  };
16
18
  costUsd: number;
17
19
  toolsUsed: string[];
18
20
  turnCount: number;
21
+ stopReason: string;
19
22
  }
20
23
  export interface SubagentProgressEvent {
21
24
  subagentId: string;
@@ -5,7 +5,7 @@
5
5
  * from index.ts (avoids circular deps) and is fully testable in isolation.
6
6
  */
7
7
  import { randomUUID } from "node:crypto";
8
- import { LoopDetector, estimateCostUsd, sanitizeError, isRetryableError, } from "../../shared/agent-core.js";
8
+ import { LoopDetector, estimateCostUsd, sanitizeError, isRetryableError, addPromptCaching, } from "../../shared/agent-core.js";
9
9
  import { buildAPIRequest } from "../../shared/api-client.js";
10
10
  import { MODELS, MODEL_MAP } from "../../shared/constants.js";
11
11
  import { dispatchTools } from "../../shared/tool-dispatch.js";
@@ -90,6 +90,8 @@ export async function runServerSubagent(opts) {
90
90
  turn++;
91
91
  loopDetector.resetTurn();
92
92
  onProgress?.({ subagentId, event: "turn", turn, maxTurns });
93
+ // Apply prompt caching to tools and messages for cache hits on repeated context
94
+ const { tools: cachedTools, messages: cachedMessages } = addPromptCaching(tools, messages);
93
95
  // Non-streaming API call with retry
94
96
  let response;
95
97
  try {
@@ -99,8 +101,8 @@ export async function runServerSubagent(opts) {
99
101
  max_tokens: apiConfig.maxTokens,
100
102
  temperature: shouldThink ? 1 : 0.3, // Anthropic requires temp=1 with thinking
101
103
  system,
102
- tools: tools,
103
- messages: messages,
104
+ tools: cachedTools,
105
+ messages: cachedMessages,
104
106
  betas: apiConfig.betas,
105
107
  context_management: apiConfig.contextManagement,
106
108
  ...(apiConfig.thinking ? { thinking: apiConfig.thinking } : {}),
@@ -178,10 +180,11 @@ function makeResult(success, output, inputTokens, outputTokens, modelId, toolsUs
178
180
  return {
179
181
  success,
180
182
  output,
181
- tokensUsed: { input: inputTokens, output: outputTokens },
183
+ tokensUsed: { input: inputTokens, output: outputTokens, cacheRead: cacheReadTokens, cacheCreation: cacheCreationTokens },
182
184
  costUsd: estimateCostUsd(inputTokens, outputTokens, modelId, 0, cacheReadTokens, cacheCreationTokens),
183
185
  toolsUsed: [...new Set(toolsUsed)],
184
186
  turnCount,
187
+ stopReason: "end_turn", // subagent always runs to completion or error
185
188
  };
186
189
  }
187
190
  async function withSubagentRetry(fn, maxRetries = 2) {
@@ -1,13 +1,53 @@
1
- // lib/supabase-client.ts — Resilient Supabase client with retry logic
1
+ // lib/supabase-client.ts — Resilient Supabase client with circuit breaker
2
2
  // Fixes intermittent 520 errors from Cloudflare by retrying failed requests
3
3
  // and reusing client instances instead of creating new ones per request.
4
+ // Circuit breaker prevents retry storms when Supabase is down.
4
5
  import { createClient } from "@supabase/supabase-js";
5
6
  const MAX_RETRIES = 3;
6
7
  const INITIAL_BACKOFF_MS = 500;
7
8
  const MAX_BACKOFF_MS = 5_000;
8
- /** Custom fetch with retry for 5xx errors (Cloudflare 520/522/524) */
9
+ // ── Circuit breaker ──
10
+ // When Supabase is consistently failing, stop retrying to prevent:
11
+ // 1. Retry storms that make Supabase worse
12
+ // 2. Event loop blocking that causes health check failures
13
+ // 3. SSE stream stalls that freeze the CLI
14
+ const CIRCUIT_FAILURE_THRESHOLD = 10; // Open circuit after 10 consecutive failures
15
+ const CIRCUIT_RESET_MS = 30_000; // Try again after 30s
16
+ let circuitFailures = 0;
17
+ let circuitOpenUntil = 0;
18
+ /** Check if circuit breaker allows the request */
19
+ function isCircuitOpen() {
20
+ if (circuitFailures < CIRCUIT_FAILURE_THRESHOLD)
21
+ return false;
22
+ if (Date.now() >= circuitOpenUntil) {
23
+ // Half-open: allow one probe request
24
+ return false;
25
+ }
26
+ return true;
27
+ }
28
+ function recordCircuitFailure() {
29
+ circuitFailures++;
30
+ if (circuitFailures >= CIRCUIT_FAILURE_THRESHOLD) {
31
+ circuitOpenUntil = Date.now() + CIRCUIT_RESET_MS;
32
+ if (circuitFailures === CIRCUIT_FAILURE_THRESHOLD) {
33
+ console.warn(`[supabase] Circuit breaker OPEN — ${circuitFailures} consecutive failures, pausing for ${CIRCUIT_RESET_MS / 1000}s`);
34
+ }
35
+ }
36
+ }
37
+ function recordCircuitSuccess() {
38
+ if (circuitFailures > 0) {
39
+ console.info(`[supabase] Circuit breaker reset — connection recovered`);
40
+ }
41
+ circuitFailures = 0;
42
+ circuitOpenUntil = 0;
43
+ }
44
+ /** Custom fetch with retry for 5xx errors + circuit breaker */
9
45
  function createRetryFetch(maxRetries = MAX_RETRIES) {
10
46
  return async (input, init) => {
47
+ // Circuit breaker: fail fast when Supabase is known to be down
48
+ if (isCircuitOpen()) {
49
+ throw new Error("Supabase circuit breaker open — skipping request");
50
+ }
11
51
  let lastError = null;
12
52
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
13
53
  try {
@@ -18,17 +58,25 @@ function createRetryFetch(maxRetries = MAX_RETRIES) {
18
58
  });
19
59
  // Retry on 5xx errors (Cloudflare 520 = origin error, 522 = timeout, 524 = timeout)
20
60
  if (res.status >= 500 && attempt < maxRetries) {
61
+ recordCircuitFailure();
62
+ if (isCircuitOpen()) {
63
+ return res; // Don't retry if circuit just opened
64
+ }
21
65
  const backoff = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
22
66
  console.warn(`[supabase] ${res.status} on ${typeof input === 'string' ? input.split('?')[0] : 'request'}, retry ${attempt + 1}/${maxRetries} in ${backoff}ms`);
23
67
  await new Promise(r => setTimeout(r, backoff));
24
68
  continue;
25
69
  }
70
+ if (res.status < 500) {
71
+ recordCircuitSuccess();
72
+ }
26
73
  return res;
27
74
  }
28
75
  catch (err) {
29
76
  lastError = err;
77
+ recordCircuitFailure();
30
78
  // Retry on network errors (ECONNRESET, ETIMEDOUT, etc.)
31
- if (attempt < maxRetries) {
79
+ if (attempt < maxRetries && !isCircuitOpen()) {
32
80
  const backoff = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
33
81
  console.warn(`[supabase] Network error: ${lastError.message}, retry ${attempt + 1}/${maxRetries} in ${backoff}ms`);
34
82
  await new Promise(r => setTimeout(r, backoff));
@@ -159,14 +159,24 @@ export function evaluateCondition(expression, ctx) {
159
159
  [/\scontains\s/, "contains"],
160
160
  [/\s!contains\s/, "!contains"],
161
161
  ];
162
+ // Identify quoted regions to avoid matching operators inside them
163
+ const quoteRegions = [];
164
+ const quoteRe = /(['"])(?:(?!\1).)*\1/g;
165
+ let qm;
166
+ while ((qm = quoteRe.exec(expr)) !== null) {
167
+ quoteRegions.push([qm.index, qm.index + qm[0].length]);
168
+ }
162
169
  for (const [pattern, op] of operatorPatterns) {
163
- // Use the LAST match: resolved template data (left side) may contain
164
- // operator-like strings, but the actual operator is the rightmost one.
170
+ // Use the LAST match that is NOT inside a quoted string.
165
171
  const globalRe = new RegExp(pattern.source, "g");
166
172
  let lastMatch = null;
167
173
  let m;
168
- while ((m = globalRe.exec(expr)) !== null)
169
- lastMatch = m;
174
+ while ((m = globalRe.exec(expr)) !== null) {
175
+ const idx = m.index;
176
+ const insideQuotes = quoteRegions.some(([s, e]) => idx > s && idx < e);
177
+ if (!insideQuotes)
178
+ lastMatch = m;
179
+ }
170
180
  if (!lastMatch || lastMatch.index === undefined)
171
181
  continue;
172
182
  let left = expr.substring(0, lastMatch.index).trim();
@@ -114,6 +114,21 @@ export function summarizeResult(toolName, action, data) {
114
114
  if (action === "browse")
115
115
  return { categories: d.data ? d.data.categories?.length || 0 : 0, status_summary: d.data ? d.data.product_status_summary : undefined };
116
116
  return { action, count: Array.isArray(d) ? d.length : (d.count ?? (d.products ? d.products.length : (d.id ? 1 : 0))) };
117
+ case "media": {
118
+ if (action === "list" || action === "search")
119
+ return { count: d.count, total: d.total };
120
+ if (action === "analytics")
121
+ return { total_items: d.total_items, orphan_count: d.orphan_count, storage_mb: d.total_storage_mb };
122
+ if (action === "upload")
123
+ return { media_id: d.media_id, file_name: d.file_name };
124
+ if (action === "bulk_upload")
125
+ return { uploaded_count: d.uploaded_count, failed_count: d.failed_count };
126
+ if (action === "bulk_update")
127
+ return { updated_count: d.updated_count, failed_count: d.failed_count };
128
+ if (action === "usage")
129
+ return { reference_count: d.reference_count };
130
+ return { action };
131
+ }
117
132
  case "audit_trail":
118
133
  return { count: d.count, days: d.days, actions: d.summary ? Object.keys(d.summary).length : 0 };
119
134
  case "telemetry":
@@ -16,6 +16,7 @@ export interface LocalAgent {
16
16
  ws: WebSocket;
17
17
  storeId: string;
18
18
  userId: string | null;
19
+ nodeId: string | null;
19
20
  capabilities: string[];
20
21
  connectedAt: Date;
21
22
  lastPong: number;
@@ -50,6 +51,7 @@ export declare function getAgentInfo(storeId: string): Array<{
50
51
  capabilities: string[];
51
52
  connected_at: string;
52
53
  uptime_seconds: number;
54
+ node_id: string | null;
53
55
  }>;
54
56
  /**
55
57
  * Execute a command on the user's local machine via their connected agent.
@@ -79,4 +81,46 @@ export declare function getGatewayStats(): {
79
81
  pending_requests: number;
80
82
  agents_by_store: Record<string, number>;
81
83
  };
84
+ /**
85
+ * Get active remote desktop sessions.
86
+ */
87
+ export declare function getRemoteDesktopSessions(): Array<{
88
+ session_id: string;
89
+ store_id: string;
90
+ user_id: string;
91
+ agent_id: string;
92
+ started_at: string;
93
+ frames_relayed: number;
94
+ bytes_relayed: number;
95
+ }>;
96
+ /**
97
+ * Get active portal sessions.
98
+ */
99
+ export declare function getPortalSessions(): Array<{
100
+ session_id: string;
101
+ store_id: string;
102
+ initiator_agent_id: string;
103
+ target_agent_id: string;
104
+ capabilities: string[];
105
+ started_at: string;
106
+ bytes_relayed: number;
107
+ }>;
108
+ export interface ClusterCommandArgs {
109
+ action: string;
110
+ task?: string;
111
+ max_cells?: number;
112
+ working_directory?: string;
113
+ cluster_id?: string;
114
+ channel_id?: string;
115
+ conversation_id?: string;
116
+ }
117
+ /**
118
+ * Execute a cluster command on the connected whale-node.
119
+ * Sends via WebSocket and waits for the result.
120
+ */
121
+ export declare function executeClusterCommand(storeId: string, args: ClusterCommandArgs, options?: {
122
+ timeout?: number;
123
+ agent_id?: string;
124
+ node_id?: string;
125
+ }): Promise<AgentResult>;
82
126
  export declare function shutdownGateway(): void;