whale-code 6.4.0 → 6.5.1

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 +51 -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 +65 -8
  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 +7 -6
  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 +85 -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 +46 -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 +36 -17
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +9 -6
  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 +25 -2
  180. package/dist/shared/agent-core.js +66 -5
  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 +15 -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
@@ -20,6 +20,151 @@ const GEMINI_MAX_TOKENS = 65536;
20
20
  const OPENAI_MAX_TOKENS = 128000;
21
21
  const OPENAI_REASONING_MAX_TOKENS = 100000;
22
22
  // ============================================================================
23
+ // IMAGE MIME TYPE CORRECTION
24
+ // ============================================================================
25
+ // Detects actual image format from base64 magic bytes and corrects media_type
26
+ // mismatches. Prevents Anthropic API 400 errors that permanently poison the
27
+ // conversation when a file extension doesn't match the actual image content
28
+ // (e.g. a .webp file that's actually PNG).
29
+ const IMAGE_SIGNATURES = [
30
+ { prefix: "iVBORw0KGgo", type: "image/png" },
31
+ { prefix: "/9j/", type: "image/jpeg" },
32
+ { prefix: "R0lGOD", type: "image/gif" },
33
+ { prefix: "UklGR", type: "image/webp" },
34
+ ];
35
+ function detectImageType(base64Data) {
36
+ for (const sig of IMAGE_SIGNATURES) {
37
+ if (base64Data.startsWith(sig.prefix)) {
38
+ // RIFF header could be WebP or other formats — verify WEBP marker at offset 8
39
+ if (sig.prefix === "UklGR") {
40
+ try {
41
+ const buf = Buffer.from(base64Data.slice(0, 24), "base64");
42
+ if (buf.length >= 12 && buf.toString("ascii", 8, 12) === "WEBP") {
43
+ return "image/webp";
44
+ }
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ return null;
50
+ }
51
+ return sig.type;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ function fixImageBlock(block) {
57
+ if (block.type === "image" && block.source?.type === "base64" && block.source?.data) {
58
+ const actual = detectImageType(block.source.data);
59
+ if (actual && actual !== block.source.media_type) {
60
+ console.log(`[proxy] Fixed image MIME: declared=${block.source.media_type} actual=${actual}`);
61
+ block.source.media_type = actual;
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ function fixImageMimeTypes(messages) {
68
+ let fixes = 0;
69
+ for (const msg of messages) {
70
+ if (!Array.isArray(msg.content))
71
+ continue;
72
+ for (const block of msg.content) {
73
+ if (fixImageBlock(block))
74
+ fixes++;
75
+ // Also check tool_result blocks — images from Read tool end up here
76
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
77
+ for (const sub of block.content) {
78
+ if (fixImageBlock(sub))
79
+ fixes++;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ if (fixes > 0) {
85
+ console.log(`[proxy] Corrected ${fixes} image MIME type(s)`);
86
+ }
87
+ }
88
+ // ============================================================================
89
+ // IMAGE PRUNING — strip base64 from old turns to keep conversation size bounded
90
+ // ============================================================================
91
+ // Conversation history with many images grows unboundedly. We keep full image
92
+ // data only in the most recent user turn; older turns get their base64 replaced
93
+ // with a lightweight "[image]" text placeholder. The model already processed
94
+ // those images — it doesn't need the raw bytes again.
95
+ function replaceImageBlock(block) {
96
+ // Replace a base64 image block with a text placeholder.
97
+ // Matches both full-data blocks AND blocks whose data was zeroed by the raw pruner.
98
+ if (block.type === "image" && block.source?.type === "base64") {
99
+ block.type = "text";
100
+ block.text = "[image]";
101
+ delete block.source;
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+ function pruneOldImages(messages) {
107
+ // Find index of last user message — that turn keeps full image data
108
+ let lastUserIdx = -1;
109
+ for (let i = messages.length - 1; i >= 0; i--) {
110
+ if (messages[i].role === "user") {
111
+ lastUserIdx = i;
112
+ break;
113
+ }
114
+ }
115
+ let pruned = 0;
116
+ for (let i = 0; i < messages.length; i++) {
117
+ if (i === lastUserIdx)
118
+ continue; // keep latest user turn intact
119
+ const msg = messages[i];
120
+ if (!Array.isArray(msg.content))
121
+ continue;
122
+ for (const block of msg.content) {
123
+ if (replaceImageBlock(block)) {
124
+ pruned++;
125
+ continue;
126
+ }
127
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
128
+ for (const sub of block.content) {
129
+ if (replaceImageBlock(sub))
130
+ pruned++;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ if (pruned > 0)
136
+ console.log(`[proxy] Pruned ${pruned} base64 image(s) from old conversation turns`);
137
+ }
138
+ /** Remove any image blocks with empty/missing data — left by the raw body pruner.
139
+ * These would cause a 400 from Anthropic regardless of which turn they're in. */
140
+ function removeEmptyImageBlocks(messages) {
141
+ let removed = 0;
142
+ for (const msg of messages) {
143
+ if (!Array.isArray(msg.content))
144
+ continue;
145
+ for (const block of msg.content) {
146
+ if (block.type === "image" && block.source?.type === "base64" && !block.source?.data) {
147
+ block.type = "text";
148
+ block.text = "[image]";
149
+ delete block.source;
150
+ removed++;
151
+ }
152
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
153
+ for (const sub of block.content) {
154
+ if (sub.type === "image" && sub.source?.type === "base64" && !sub.source?.data) {
155
+ sub.type = "text";
156
+ sub.text = "[image]";
157
+ delete sub.source;
158
+ removed++;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ if (removed > 0)
165
+ console.log(`[proxy] Removed ${removed} empty image block(s) from messages`);
166
+ }
167
+ // ============================================================================
23
168
  // MAIN PROXY DISPATCHER
24
169
  // ============================================================================
25
170
  export async function handleProxy(res, body, corsHeaders) {
@@ -28,6 +173,12 @@ export async function handleProxy(res, body, corsHeaders) {
28
173
  jsonResponse(res, 400, { error: "messages array required" }, corsHeaders);
29
174
  return;
30
175
  }
176
+ // Fix mismatched image MIME types before they hit the API
177
+ fixImageMimeTypes(messages);
178
+ // Strip base64 data from old turns — keeps request size bounded across long conversations
179
+ pruneOldImages(messages);
180
+ // Remove empty image blocks left by the raw body pruner (data:"") — would cause Anthropic 400
181
+ removeEmptyImageBlocks(messages);
31
182
  // Resolve aliases (e.g. "whale/agent" → "claude-opus-4-6", "opus" → "claude-opus-4-6")
32
183
  const resolved = MODEL_MAP[requestedModel] || requestedModel;
33
184
  const model = ALLOWED_MODELS.includes(resolved) ? resolved : MODELS.SONNET;
@@ -17,7 +17,8 @@
17
17
  * That's it — MCP, WhaleChat, workflows, CLI all pick it up automatically.
18
18
  */
19
19
  import type { SupabaseClient } from "@supabase/supabase-js";
20
- export declare function flushAuditLogs(supabase: SupabaseClient): Promise<void>;
20
+ import { flushSpans } from "./lib/clickhouse-buffer.js";
21
+ export { flushSpans };
21
22
  export declare function getToolMetrics(): Record<string, {
22
23
  invocations: number;
23
24
  errors: number;
@@ -146,4 +147,3 @@ skipAudit?: boolean): Promise<{
146
147
  error?: string;
147
148
  }>;
148
149
  export declare function loadAgentConfig(supabase: SupabaseClient, agentId: string, storeId?: string): Promise<AgentConfig | null>;
149
- export {};
@@ -35,6 +35,8 @@ import { handleWorkflows } from "./handlers/workflows.js";
35
35
  import { handleEmbeddings } from "./handlers/embeddings.js";
36
36
  import { handleLLM } from "./handlers/llm-providers.js";
37
37
  import { handleImageGen } from "./handlers/image-gen.js";
38
+ import { handleRemoveBg } from "./handlers/remove-bg.js";
39
+ import { handleMedia } from "./handlers/media.js";
38
40
  import { handleVideoGen } from "./handlers/video-gen.js";
39
41
  import { handleAPIKeys } from "./handlers/api-keys.js";
40
42
  import { handleCreations } from "./handlers/creations.js";
@@ -42,39 +44,15 @@ import { handleMetaAds } from "./handlers/meta-ads.js";
42
44
  import { handleKali } from "./handlers/kali.js";
43
45
  import { handleLocalAgent } from "./handlers/local-agent.js";
44
46
  import { handleEnrichment } from "./handlers/enrichment.js";
47
+ import { handleStorefront } from "./handlers/storefront.js";
48
+ import { handleClickHouse } from "./handlers/clickhouse.js";
45
49
  import { summarizeResult, withTimeout } from "./lib/utils.js";
46
50
  // ============================================================================
47
- // AUDIT LOG BATCHING buffer inserts and flush periodically
51
+ // TELEMETRY ClickHouse span buffer (replaces Postgres audit_logs buffer)
48
52
  // ============================================================================
49
- const auditLogBuffer = [];
50
- const AUDIT_FLUSH_INTERVAL = 500; // ms
51
- const AUDIT_FLUSH_MAX = 100; // max records before force flush
52
- let auditFlushTimer = null;
53
- export async function flushAuditLogs(supabase) {
54
- if (auditLogBuffer.length === 0)
55
- return;
56
- const batch = auditLogBuffer.splice(0, auditLogBuffer.length);
57
- try {
58
- const { error } = await supabase.from("audit_logs").insert(batch);
59
- if (error)
60
- console.error("[audit-batch] flush error:", error.message, "lost", batch.length, "records");
61
- }
62
- catch (err) {
63
- console.error("[audit-batch] flush exception:", err.message, "lost", batch.length, "records");
64
- }
65
- }
66
- function queueAuditLog(supabase, row) {
67
- auditLogBuffer.push(row);
68
- if (auditLogBuffer.length >= AUDIT_FLUSH_MAX) {
69
- flushAuditLogs(supabase);
70
- }
71
- else if (!auditFlushTimer) {
72
- auditFlushTimer = setTimeout(() => {
73
- auditFlushTimer = null;
74
- flushAuditLogs(supabase);
75
- }, AUDIT_FLUSH_INTERVAL);
76
- }
77
- }
53
+ import { queueSpan, flushSpans, auditRowToSpan, classifyErrorType } from "./lib/clickhouse-buffer.js";
54
+ // Re-export for callers that used flushAuditLogs (e.g. index.ts shutdown)
55
+ export { flushSpans };
78
56
  // ============================================================================
79
57
  // IN-MEMORY EXECUTION METRICS
80
58
  // ============================================================================
@@ -299,9 +277,10 @@ export const TOOL_HANDLERS = {
299
277
  locations: { handler: handleLocations, timeout: DEFAULT_TIMEOUT, requiresStore: true },
300
278
  suppliers: { handler: handleSuppliers, timeout: DEFAULT_TIMEOUT, requiresStore: true },
301
279
  store: { handler: handleStore, timeout: DEFAULT_TIMEOUT, requiresStore: true },
280
+ storefront: { handler: handleStorefront, timeout: DEFAULT_TIMEOUT, requiresStore: true },
302
281
  // --- Communication ---
303
282
  email: { handler: handleEmail, timeout: DEFAULT_TIMEOUT, requiresStore: true },
304
- documents: { handler: handleDocuments, timeout: DEFAULT_TIMEOUT, requiresStore: true },
283
+ documents: { handler: handleDocuments, timeout: 120_000, requiresStore: true },
305
284
  // --- Operations ---
306
285
  alerts: { handler: handleAlerts, timeout: DEFAULT_TIMEOUT, requiresStore: true },
307
286
  audit_trail: { handler: handleAuditTrail, timeout: DEFAULT_TIMEOUT, requiresStore: true },
@@ -309,6 +288,8 @@ export const TOOL_HANDLERS = {
309
288
  // --- AI & Generation ---
310
289
  voice: { handler: handleVoice, timeout: 120_000, requiresStore: true },
311
290
  image_gen: { handler: handleImageGen, timeout: 60_000, requiresStore: true },
291
+ remove_bg: { handler: handleRemoveBg, timeout: 60_000, requiresStore: true },
292
+ media: { handler: handleMedia, timeout: 60_000, requiresStore: true },
312
293
  video_gen: { handler: handleVideoGen, timeout: 600_000, requiresStore: true },
313
294
  llm: { handler: handleLLM, timeout: 120_000, requiresStore: true },
314
295
  embeddings: { handler: handleEmbeddings, timeout: 60_000, requiresStore: true },
@@ -326,6 +307,8 @@ export const TOOL_HANDLERS = {
326
307
  local_agent: { handler: handleLocalAgent, timeout: 600_000, requiresStore: false },
327
308
  // --- Customer Data Protection ---
328
309
  enrichment: { handler: handleEnrichment, timeout: 60_000, requiresStore: true },
310
+ // --- Observability (ClickHouse) ---
311
+ clickhouse: { handler: handleClickHouse, timeout: 60_000, requiresStore: false },
329
312
  // --- Meta: Tool Discovery (lazy loading) ---
330
313
  discover_tools: { handler: handleDiscoverTools, timeout: 5000, requiresStore: false },
331
314
  };
@@ -377,14 +360,14 @@ const TOOL_CATEGORIES = {
377
360
  inventory: "business", purchase_orders: "business", transfers: "business",
378
361
  products: "business", collections: "business", customers: "business",
379
362
  orders: "business", analytics: "business", locations: "business",
380
- suppliers: "business", store: "business",
363
+ suppliers: "business", store: "business", storefront: "business",
381
364
  // Communication
382
365
  email: "communication", documents: "communication",
383
366
  // Operations
384
367
  alerts: "operations", audit_trail: "operations", workflows: "operations",
385
368
  telemetry: "operations",
386
369
  // Media & AI
387
- voice: "media", image_gen: "media", video_gen: "media",
370
+ voice: "media", image_gen: "media", video_gen: "media", remove_bg: "media", media: "media",
388
371
  llm: "ai", embeddings: "ai", creations: "media",
389
372
  // Platform
390
373
  web_search: "platform", browser: "platform", discovery: "platform",
@@ -750,6 +733,9 @@ skipAudit) {
750
733
  }
751
734
  if (result.error) {
752
735
  details.tool_error = result.error;
736
+ const errorType = classifyErrorType(result.error);
737
+ if (errorType)
738
+ details.error_type = errorType;
753
739
  }
754
740
  const bytes = new Uint8Array(8);
755
741
  crypto.getRandomValues(bytes);
@@ -765,9 +751,12 @@ skipAudit) {
765
751
  source: source || "fly_container",
766
752
  details,
767
753
  error_message: result.error || null,
754
+ error_type: classifyErrorType(result.error) || undefined,
768
755
  duration_ms: endTime - startTime,
769
756
  user_id: userId || null,
770
757
  user_email: userEmail || null,
758
+ input_bytes: inputBytes,
759
+ output_bytes: outputBytes,
771
760
  // OTEL fields
772
761
  trace_id: traceId || null,
773
762
  span_id: spanId,
@@ -777,7 +766,7 @@ skipAudit) {
777
766
  start_time: new Date(startTime).toISOString(),
778
767
  end_time: new Date(endTime).toISOString(),
779
768
  };
780
- queueAuditLog(supabase, auditRow);
769
+ queueSpan(auditRowToSpan(auditRow));
781
770
  }
782
771
  catch (err) {
783
772
  console.error("[audit] exception:", err);
@@ -788,11 +777,12 @@ skipAudit) {
788
777
  // AGENT LOADER
789
778
  // ============================================================================
790
779
  export async function loadAgentConfig(supabase, agentId, storeId) {
780
+ // storeId is required for tenant isolation — only omit for internal/migration callers
791
781
  let query = supabase
792
782
  .from("ai_agent_config")
793
783
  .select("*")
794
784
  .eq("id", agentId);
795
- // P0 FIX: Filter by store_id to prevent cross-tenant agent access
785
+ // ALWAYS filter by store_id when provided (which should be always for user requests)
796
786
  if (storeId) {
797
787
  query = query.eq("store_id", storeId);
798
788
  }
@@ -56,8 +56,8 @@ export interface ContextManagementConfig {
56
56
  export declare const COMPACTION_TRIGGER_TOKENS = 120000;
57
57
  /** Max cumulative tokens before forcing wrap-up (prevents runaway compaction cost) */
58
58
  export declare const COMPACTION_TOTAL_BUDGET = 2000000;
59
- /** Default session cost budget in USD — Infinity = no limit (budget enforcement disabled by default) */
60
- export declare const DEFAULT_SESSION_COST_BUDGET_USD: number;
59
+ /** Default session cost budget in USD — hard cap to prevent runaway spending */
60
+ export declare const DEFAULT_SESSION_COST_BUDGET_USD = 5;
61
61
  /**
62
62
  * Provider-aware compaction configuration.
63
63
  * - Anthropic/Bedrock: native server-side compaction via compact_20260112
@@ -88,6 +88,8 @@ export declare function addPromptCaching(tools: Array<Record<string, unknown>>,
88
88
  tools: Array<Record<string, unknown>>;
89
89
  messages: Array<Record<string, unknown>>;
90
90
  };
91
+ /** djb2 string hash — fast, deterministic, no dependencies */
92
+ export declare function djb2Hash(str: string): string;
91
93
  export declare class LoopDetector {
92
94
  private history;
93
95
  private consecutiveErrors;
@@ -97,16 +99,24 @@ export declare class LoopDetector {
97
99
  private failedStrategies;
98
100
  private consecutiveFailedTurns;
99
101
  private totalSessionErrors;
102
+ /** Tracks how many times the same file path has been read this session */
103
+ private fileReadCounts;
100
104
  static IDENTICAL_CALL_LIMIT: number;
101
105
  static CONSECUTIVE_ERROR_LIMIT: number;
102
106
  static TURN_ERROR_LIMIT: number;
103
107
  static WINDOW: number;
104
108
  static SESSION_TOOL_ERROR_LIMIT: number;
105
109
  static CONSECUTIVE_FAILED_TURN_LIMIT: number;
110
+ static FILE_READ_LIMIT: number;
106
111
  /** Get the error-tracking key for a tool call. Tools with an `action` param
107
112
  * are tracked per-action so e.g. voice/speak failing won't block voice/music_compose. */
108
113
  private errorKey;
109
114
  recordCall(name: string, input: Record<string, unknown>): LoopCheckResult;
115
+ /**
116
+ * Track file read frequency — call when the tool is known to be a file read.
117
+ * Blocks re-reading the same path more than FILE_READ_LIMIT times per session.
118
+ */
119
+ trackRead(path: string): LoopCheckResult;
110
120
  recordResult(name: string, success: boolean, input?: Record<string, unknown>): void;
111
121
  endTurn(): BailCheckResult;
112
122
  resetTurn(): void;
@@ -136,6 +146,11 @@ export declare const MODEL_PRICING: Record<string, {
136
146
  outputPer1M: number;
137
147
  thinkingPer1M?: number;
138
148
  }>;
149
+ /**
150
+ * Emit graduated cost warnings at 25%, 50%, 75% thresholds.
151
+ * Single source of truth — replaces copy-pasted blocks in server + CLI.
152
+ */
153
+ export declare function emitCostWarningIfNeeded(sessionCostUsd: number, maxCostUsd: number, costWarningsEmitted: Set<number>, onText?: (text: string) => void): void;
139
154
  export declare function estimateCostUsd(inputTokens: number, outputTokens: number, model: string, thinkingTokens?: number, cacheReadTokens?: number, cacheCreationTokens?: number): number;
140
155
  /**
141
156
  * Route to cheaper model when the task is simple enough.
@@ -154,4 +169,12 @@ export declare function truncateToolResult(content: string, maxChars: number): s
154
169
  export declare function getMaxToolResultChars(contextConfig?: {
155
170
  max_tool_result_chars?: number;
156
171
  } | null): number;
172
+ /**
173
+ * Demote subagent model requests — single source of truth for server + CLI.
174
+ * - explore/research: always haiku
175
+ * - opus: demoted to sonnet
176
+ * - sonnet: kept for plan, demoted to haiku for others
177
+ * - default/undefined: haiku
178
+ */
179
+ export declare function demoteSubagentModel(requested: string | undefined, agentType?: string): "haiku" | "sonnet";
157
180
  export declare function sanitizeError(err: unknown): string;
@@ -25,6 +25,14 @@ export function resolveToolChoice(opts) {
25
25
  return "none";
26
26
  }
27
27
  }
28
+ // 2b. Alternating pattern detection: catch A→B→A→B→A→B death spirals (e.g. read→edit→read→edit)
29
+ if (opts.recentToolUses.length >= 6) {
30
+ const last6 = opts.recentToolUses.slice(-6);
31
+ const isRepeatingPair = last6[2] === last6[0] && last6[3] === last6[1] &&
32
+ last6[4] === last6[0] && last6[5] === last6[1];
33
+ if (isRepeatingPair)
34
+ return "none";
35
+ }
28
36
  // 3. Keyword matching: check if the user message mentions a specific tool name
29
37
  // Only on the first turn (avoids false positives on multi-turn conversations)
30
38
  if (opts.turnCount === 1 && opts.userMessage && opts.availableToolNames.length > 0) {
@@ -47,8 +55,8 @@ export function resolveToolChoice(opts) {
47
55
  export const COMPACTION_TRIGGER_TOKENS = 120_000;
48
56
  /** Max cumulative tokens before forcing wrap-up (prevents runaway compaction cost) */
49
57
  export const COMPACTION_TOTAL_BUDGET = 2_000_000;
50
- /** Default session cost budget in USD — Infinity = no limit (budget enforcement disabled by default) */
51
- export const DEFAULT_SESSION_COST_BUDGET_USD = Infinity;
58
+ /** Default session cost budget in USD — hard cap to prevent runaway spending */
59
+ export const DEFAULT_SESSION_COST_BUDGET_USD = 5.00;
52
60
  export function getCompactionConfig(model) {
53
61
  const provider = getProvider(model);
54
62
  switch (provider) {
@@ -187,7 +195,7 @@ export function addPromptCaching(tools, messages) {
187
195
  // LOOP DETECTION
188
196
  // ============================================================================
189
197
  /** djb2 string hash — fast, deterministic, no dependencies */
190
- function djb2Hash(str) {
198
+ export function djb2Hash(str) {
191
199
  let hash = 5381;
192
200
  for (let i = 0; i < str.length; i++) {
193
201
  hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;
@@ -203,12 +211,15 @@ export class LoopDetector {
203
211
  failedStrategies = new Set();
204
212
  consecutiveFailedTurns = 0;
205
213
  totalSessionErrors = 0;
214
+ /** Tracks how many times the same file path has been read this session */
215
+ fileReadCounts = new Map();
206
216
  static IDENTICAL_CALL_LIMIT = 4;
207
217
  static CONSECUTIVE_ERROR_LIMIT = 3;
208
218
  static TURN_ERROR_LIMIT = 5;
209
219
  static WINDOW = 20;
210
220
  static SESSION_TOOL_ERROR_LIMIT = 10;
211
221
  static CONSECUTIVE_FAILED_TURN_LIMIT = 3;
222
+ static FILE_READ_LIMIT = 3;
212
223
  /** Get the error-tracking key for a tool call. Tools with an `action` param
213
224
  * are tracked per-action so e.g. voice/speak failing won't block voice/music_compose. */
214
225
  errorKey(name, input) {
@@ -259,6 +270,21 @@ export class LoopDetector {
259
270
  }
260
271
  return { blocked: false };
261
272
  }
273
+ /**
274
+ * Track file read frequency — call when the tool is known to be a file read.
275
+ * Blocks re-reading the same path more than FILE_READ_LIMIT times per session.
276
+ */
277
+ trackRead(path) {
278
+ const readCount = (this.fileReadCounts.get(path) || 0) + 1;
279
+ this.fileReadCounts.set(path, readCount);
280
+ if (readCount > LoopDetector.FILE_READ_LIMIT) {
281
+ return {
282
+ blocked: true,
283
+ reason: `File "${path}" already read ${readCount - 1} times this session. Use the content from a previous read instead of re-reading.`,
284
+ };
285
+ }
286
+ return { blocked: false };
287
+ }
262
288
  recordResult(name, success, input) {
263
289
  const eKey = this.errorKey(name, input);
264
290
  if (success) {
@@ -319,15 +345,18 @@ export class LoopDetector {
319
345
  return { shouldBail: false };
320
346
  }
321
347
  resetTurn() {
322
- this.history = [];
323
- this.consecutiveErrors.clear();
348
+ // Only reset per-turn counters — history and consecutiveErrors persist
349
+ // for cross-turn detection. The full reset() clears everything for new sessions.
324
350
  this.turnErrors = 0;
325
351
  this.turnHadErrors = false;
326
352
  }
327
353
  reset() {
328
354
  this.resetTurn();
355
+ this.history = [];
356
+ this.consecutiveErrors.clear();
329
357
  this.sessionErrors.clear();
330
358
  this.failedStrategies.clear();
359
+ this.fileReadCounts.clear();
331
360
  this.consecutiveFailedTurns = 0;
332
361
  this.totalSessionErrors = 0;
333
362
  }
@@ -419,6 +448,20 @@ export const MODEL_PRICING = {
419
448
  "o3-mini": { inputPer1M: 1.10, outputPer1M: 4.40, thinkingPer1M: 4.40 },
420
449
  "o4-mini": { inputPer1M: 1.10, outputPer1M: 4.40, thinkingPer1M: 4.40 },
421
450
  };
451
+ /**
452
+ * Emit graduated cost warnings at 25%, 50%, 75% thresholds.
453
+ * Single source of truth — replaces copy-pasted blocks in server + CLI.
454
+ */
455
+ export function emitCostWarningIfNeeded(sessionCostUsd, maxCostUsd, costWarningsEmitted, onText) {
456
+ if (!isFinite(maxCostUsd) || !onText)
457
+ return;
458
+ for (const pct of [25, 50, 75]) {
459
+ if (!costWarningsEmitted.has(pct) && sessionCostUsd >= maxCostUsd * (pct / 100)) {
460
+ costWarningsEmitted.add(pct);
461
+ onText(`\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${maxCostUsd.toFixed(2)}).${pct >= 75 ? " Wrap up soon." : ""}]`);
462
+ }
463
+ }
464
+ }
422
465
  export function estimateCostUsd(inputTokens, outputTokens, model, thinkingTokens = 0, cacheReadTokens = 0, cacheCreationTokens = 0) {
423
466
  // Exact match first, then find a pricing key that is a prefix of the model ID
424
467
  const pricing = MODEL_PRICING[model]
@@ -521,6 +564,24 @@ export function getMaxToolResultChars(contextConfig) {
521
564
  // ============================================================================
522
565
  // UTILITY — sanitize errors (strip API keys, passwords)
523
566
  // ============================================================================
567
+ /**
568
+ * Demote subagent model requests — single source of truth for server + CLI.
569
+ * - explore/research: always haiku
570
+ * - opus: demoted to sonnet
571
+ * - sonnet: kept for plan, demoted to haiku for others
572
+ * - default/undefined: haiku
573
+ */
574
+ export function demoteSubagentModel(requested, agentType) {
575
+ if (agentType === "explore" || agentType === "research")
576
+ return "haiku";
577
+ if (!requested)
578
+ return "haiku";
579
+ if (requested === "opus")
580
+ return "sonnet";
581
+ if (requested === "sonnet")
582
+ return agentType === "plan" ? "sonnet" : "haiku";
583
+ return "haiku";
584
+ }
524
585
  export function sanitizeError(err) {
525
586
  const msg = String(err);
526
587
  return msg
@@ -50,6 +50,18 @@ export function buildAPIRequest(opts) {
50
50
  keep: { type: "tool_uses", value: 2 },
51
51
  },
52
52
  ];
53
+ // Compaction for sub-agents on models that support it (lower threshold than main agent).
54
+ // Trigger at 80K — above the 60K clear_tool_uses threshold so clearing fires first.
55
+ const supportsCompaction = model.includes("opus-4-6") || model.includes("sonnet-4-6");
56
+ if (supportsCompaction) {
57
+ edits.push({
58
+ type: "compact_20260112",
59
+ trigger: { type: "input_tokens", value: 80_000 },
60
+ pause_after_compaction: true,
61
+ instructions: "Summarize preserving: task goal, files found/modified, key findings, next steps.",
62
+ });
63
+ betas.push("compact-2026-01-12");
64
+ }
53
65
  }
54
66
  break;
55
67
  case "teammate": {
@@ -113,16 +125,46 @@ export async function callServerProxy(config) {
113
125
  if (config.storeId) {
114
126
  body.store_id = config.storeId;
115
127
  }
128
+ // Prune old base64 images from conversation history before serializing.
129
+ // Keep images only in the last 2 messages to avoid multi-MB request bodies
130
+ // that can cause "Invalid JSON" errors from body truncation on the proxy.
131
+ if (Array.isArray(body.messages) && body.messages.length > 4) {
132
+ const msgs = body.messages;
133
+ const keepFrom = Math.max(0, msgs.length - 2);
134
+ for (let i = 0; i < keepFrom; i++) {
135
+ const msg = msgs[i];
136
+ if (!Array.isArray(msg?.content))
137
+ continue;
138
+ for (const block of msg.content) {
139
+ if (block.type === "image" && block.source?.type === "base64") {
140
+ block.type = "text";
141
+ block.text = "[image]";
142
+ delete block.source;
143
+ }
144
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
145
+ for (const sub of block.content) {
146
+ if (sub.type === "image" && sub.source?.type === "base64") {
147
+ sub.type = "text";
148
+ sub.text = "[image]";
149
+ delete sub.source;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ const serialized = JSON.stringify(body);
116
157
  const fetchOpts = {
117
158
  method: "POST",
118
159
  headers: {
119
160
  "Content-Type": "application/json",
120
161
  "Authorization": `Bearer ${config.token}`,
162
+ "Content-Length": String(Buffer.byteLength(serialized)),
121
163
  },
122
- body: JSON.stringify(body),
164
+ body: serialized,
123
165
  signal,
124
166
  };
125
- // Apply timeout if specified
167
+ // Apply timeout only when the caller did NOT already supply a signal
126
168
  let controller;
127
169
  let timeout;
128
170
  if (timeoutMs && !signal) {
@@ -162,7 +204,16 @@ export async function callServerProxy(config) {
162
204
  const errMsg = err instanceof Error ? err.message : String(err);
163
205
  config.onRetry?.(attempt + 1, MAX_RETRIES, errMsg);
164
206
  const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
165
- await new Promise((resolve) => setTimeout(resolve, delay));
207
+ // Abort-aware backoff ESC can interrupt the wait immediately
208
+ await new Promise((resolve, reject) => {
209
+ if (signal?.aborted) {
210
+ reject(signal.reason ?? new Error("Aborted"));
211
+ return;
212
+ }
213
+ const timer = setTimeout(resolve, delay);
214
+ const onAbort = () => { clearTimeout(timer); reject(signal.reason ?? new Error("Aborted")); };
215
+ signal?.addEventListener("abort", onAbort, { once: true });
216
+ });
166
217
  // Fallback model on last retry
167
218
  if (attempt === MAX_RETRIES - 1 && config.fallbackModel) {
168
219
  const fromModel = config.model;
@@ -13,7 +13,7 @@ import type { StreamResult, StreamCallbacks } from "./types.js";
13
13
  * Parse SSE stream from proxy HTTP response into typed events.
14
14
  * Handles `data: {...}\n\n` format with [DONE] sentinel.
15
15
  */
16
- export declare function parseSSEStream(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<BetaStreamEvent>;
16
+ export declare function parseSSEStream(body: ReadableStream<Uint8Array>, signal?: AbortSignal, readTimeoutMs?: number): AsyncGenerator<BetaStreamEvent>;
17
17
  /**
18
18
  * Collect all events into a StreamResult. No callbacks — used by
19
19
  * subagent and teammate where real-time text isn't needed.
@@ -14,7 +14,7 @@
14
14
  * Parse SSE stream from proxy HTTP response into typed events.
15
15
  * Handles `data: {...}\n\n` format with [DONE] sentinel.
16
16
  */
17
- export async function* parseSSEStream(body, signal) {
17
+ export async function* parseSSEStream(body, signal, readTimeoutMs = 90_000) {
18
18
  const reader = body.getReader();
19
19
  const decoder = new TextDecoder();
20
20
  let buffer = "";
@@ -22,7 +22,10 @@ export async function* parseSSEStream(body, signal) {
22
22
  while (true) {
23
23
  if (signal?.aborted)
24
24
  break;
25
- const { done, value } = await reader.read();
25
+ // Read with timeout abort if no data arrives within readTimeoutMs
26
+ const readPromise = reader.read();
27
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`SSE stream stalled — no data for ${readTimeoutMs / 1000}s`)), readTimeoutMs));
28
+ const { done, value } = await Promise.race([readPromise, timeoutPromise]);
26
29
  if (done)
27
30
  break;
28
31
  buffer += decoder.decode(value, { stream: true });