whale-code 6.5.3 → 6.5.5

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.
@@ -2,6 +2,20 @@
2
2
  // Supports linking keys to creations (TV menus, displays, landing pages)
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  const KEY_COLS = "id, name, key_prefix, key_type, scope, is_active, rate_limit_per_minute, rate_limit_per_day, last_used_at, request_count, expires_at, revoked_at, revoked_reason, creation_id, client_store_id, created_at, updated_at";
5
+ // Canonical format: action:resource (e.g. "read:products", "write:cart")
6
+ const SCOPE_ACTIONS = new Set(["read", "write"]);
7
+ function normalizeScopes(scopes) {
8
+ return scopes.map(s => {
9
+ if (s === "*" || !s.includes(":"))
10
+ return s;
11
+ const [left, right] = s.split(":", 2);
12
+ if (SCOPE_ACTIONS.has(left))
13
+ return s; // already "read:products"
14
+ if (SCOPE_ACTIONS.has(right))
15
+ return `${right}:${left}`; // "products:read" → "read:products"
16
+ return s;
17
+ });
18
+ }
5
19
  export async function handleAPIKeys(sb, args, storeId) {
6
20
  const sid = storeId;
7
21
  const action = args.action;
@@ -15,7 +29,7 @@ export async function handleAPIKeys(sb, args, storeId) {
15
29
  if (keyType !== "live" && keyType !== "test") {
16
30
  return { success: false, error: "key_type must be 'live' or 'test'" };
17
31
  }
18
- const scopes = args.scopes || ["*"];
32
+ const scopes = normalizeScopes(args.scopes || ["*"]);
19
33
  const rateLimitPerMinute = args.rate_limit_per_minute || 60;
20
34
  const rateLimitPerDay = args.rate_limit_per_day || 10000;
21
35
  const expiresAt = args.expires_at;
@@ -167,7 +181,7 @@ export async function handleAPIKeys(sb, args, storeId) {
167
181
  if (args.name !== undefined)
168
182
  updates.name = args.name;
169
183
  if (args.scopes !== undefined)
170
- updates.scope = args.scopes;
184
+ updates.scope = normalizeScopes(args.scopes);
171
185
  if (args.rate_limit_per_minute !== undefined)
172
186
  updates.rate_limit_per_minute = args.rate_limit_per_minute;
173
187
  if (args.rate_limit_per_day !== undefined)
@@ -35,7 +35,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
35
35
  results?: undefined;
36
36
  errors?: undefined;
37
37
  warnings?: undefined;
38
- customers?: undefined;
39
38
  document_ids?: undefined;
40
39
  customer_id?: undefined;
41
40
  customer_email?: undefined;
@@ -73,7 +72,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
73
72
  results?: undefined;
74
73
  errors?: undefined;
75
74
  warnings?: undefined;
76
- customers?: undefined;
77
75
  document_ids?: undefined;
78
76
  customer_id?: undefined;
79
77
  customer_email?: undefined;
@@ -103,7 +101,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
103
101
  results?: undefined;
104
102
  errors?: undefined;
105
103
  warnings?: undefined;
106
- customers?: undefined;
107
104
  document_ids?: undefined;
108
105
  customer_id?: undefined;
109
106
  customer_email?: undefined;
@@ -133,7 +130,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
133
130
  results?: undefined;
134
131
  errors?: undefined;
135
132
  warnings?: undefined;
136
- customers?: undefined;
137
133
  document_ids?: undefined;
138
134
  customer_id?: undefined;
139
135
  customer_email?: undefined;
@@ -170,7 +166,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
170
166
  results?: undefined;
171
167
  errors?: undefined;
172
168
  warnings?: undefined;
173
- customers?: undefined;
174
169
  document_ids?: undefined;
175
170
  customer_id?: undefined;
176
171
  customer_email?: undefined;
@@ -200,7 +195,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
200
195
  results?: undefined;
201
196
  errors?: undefined;
202
197
  warnings?: undefined;
203
- customers?: undefined;
204
198
  document_ids?: undefined;
205
199
  customer_id?: undefined;
206
200
  customer_email?: undefined;
@@ -236,7 +230,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
236
230
  results?: undefined;
237
231
  errors?: undefined;
238
232
  warnings?: undefined;
239
- customers?: undefined;
240
233
  document_ids?: undefined;
241
234
  customer_id?: undefined;
242
235
  customer_email?: undefined;
@@ -273,7 +266,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
273
266
  results?: undefined;
274
267
  errors?: undefined;
275
268
  warnings?: undefined;
276
- customers?: undefined;
277
269
  document_ids?: undefined;
278
270
  customer_id?: undefined;
279
271
  customer_email?: undefined;
@@ -307,7 +299,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
307
299
  profiles?: undefined;
308
300
  errors?: undefined;
309
301
  warnings?: undefined;
310
- customers?: undefined;
311
302
  document_ids?: undefined;
312
303
  customer_id?: undefined;
313
304
  customer_email?: undefined;
@@ -338,7 +329,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
338
329
  succeeded?: undefined;
339
330
  failed?: undefined;
340
331
  results?: undefined;
341
- customers?: undefined;
342
332
  document_ids?: undefined;
343
333
  customer_id?: undefined;
344
334
  customer_email?: undefined;
@@ -367,7 +357,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
367
357
  failed?: undefined;
368
358
  results?: undefined;
369
359
  errors?: undefined;
370
- customers?: undefined;
371
360
  document_ids?: undefined;
372
361
  customer_id?: undefined;
373
362
  customer_email?: undefined;
@@ -404,7 +393,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
404
393
  results?: undefined;
405
394
  errors?: undefined;
406
395
  warnings?: undefined;
407
- customers?: undefined;
408
396
  document_ids?: undefined;
409
397
  customer_id?: undefined;
410
398
  customer_email?: undefined;
@@ -441,45 +429,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
441
429
  results?: undefined;
442
430
  errors?: undefined;
443
431
  warnings?: undefined;
444
- customers?: undefined;
445
- document_ids?: undefined;
446
- customer_id?: undefined;
447
- customer_email?: undefined;
448
- email_sent?: undefined;
449
- };
450
- error?: undefined;
451
- } | {
452
- success: boolean;
453
- data: {
454
- count: number;
455
- customers: {
456
- id: any;
457
- name: string;
458
- email: any;
459
- phone: any;
460
- loyalty_tier: any;
461
- total_orders: any;
462
- is_active: any;
463
- }[];
464
- id?: undefined;
465
- name?: undefined;
466
- type?: undefined;
467
- url?: undefined;
468
- file_name?: undefined;
469
- size?: undefined;
470
- documents?: undefined;
471
- deleted?: undefined;
472
- template_id?: undefined;
473
- templates?: undefined;
474
- template?: undefined;
475
- stores?: undefined;
476
- profiles?: undefined;
477
- total?: undefined;
478
- succeeded?: undefined;
479
- failed?: undefined;
480
- results?: undefined;
481
- errors?: undefined;
482
- warnings?: undefined;
483
432
  document_ids?: undefined;
484
433
  customer_id?: undefined;
485
434
  customer_email?: undefined;
@@ -513,7 +462,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
513
462
  results?: undefined;
514
463
  errors?: undefined;
515
464
  warnings?: undefined;
516
- customers?: undefined;
517
465
  };
518
466
  error?: undefined;
519
467
  } | {
@@ -548,7 +496,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
548
496
  results?: undefined;
549
497
  errors?: undefined;
550
498
  warnings?: undefined;
551
- customers?: undefined;
552
499
  document_ids?: undefined;
553
500
  customer_id?: undefined;
554
501
  customer_email?: undefined;
@@ -4,9 +4,26 @@ import { applyGenerationRules, generateCannabinoidData, applyCalculations, runVa
4
4
  import { renderLayoutToPdf, renderHtmlToPdf, renderLabelToPdf } from "../lib/react-pdf-layout.js";
5
5
  import { renderCOAToPdf } from "../lib/coa-renderer.js";
6
6
  import QRCode from "qrcode";
7
+ import { handleBrowser } from "./browser.js";
7
8
  const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB per attachment
8
9
  const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25MB total
9
10
  const MAX_ATTACHMENT_COUNT = 10;
11
+ /** Generate a PNG thumbnail for a PDF document via Playwright screenshot and store it in Supabase. */
12
+ async function generateThumbnail(sb, docId, pdfUrl) {
13
+ // Wrap in Google Docs viewer — headless Chromium downloads raw PDFs instead of rendering them
14
+ const viewerUrl = `https://docs.google.com/gview?url=${encodeURIComponent(pdfUrl)}&embedded=true`;
15
+ const result = await handleBrowser(sb, { action: "screenshot", url: viewerUrl });
16
+ const b64 = result.data?.screenshot_base64;
17
+ if (!b64)
18
+ return;
19
+ const thumbBuffer = Buffer.from(b64, 'base64');
20
+ const thumbPath = `thumbs/${docId}.png`;
21
+ await sb.storage.from('screenshots').upload(thumbPath, thumbBuffer, {
22
+ contentType: 'image/png', upsert: true,
23
+ });
24
+ const { data: thumbUrlData } = sb.storage.from('screenshots').getPublicUrl(thumbPath);
25
+ await sb.from('store_documents').update({ thumbnail_url: thumbUrlData.publicUrl }).eq('id', docId);
26
+ }
10
27
  export async function handleEmail(sb, args, storeId) {
11
28
  if (!storeId)
12
29
  return { success: false, error: "store_id required" };
@@ -369,6 +386,8 @@ export async function handleDocuments(sb, args, storeId) {
369
386
  }).select("id, document_name, file_url, created_at").single();
370
387
  if (insertErr)
371
388
  return { success: false, error: insertErr.message };
389
+ if (fileUrl.endsWith('.pdf'))
390
+ await generateThumbnail(sb, record.id, fileUrl).catch(() => { });
372
391
  return { success: true, data: { id: record.id, name: record.document_name, type: docType, url: record.file_url, file_name: fileName, size: sizeBytes } };
373
392
  }
374
393
  case "find": {
@@ -521,6 +540,8 @@ export async function handleDocuments(sb, args, storeId) {
521
540
  }).select("id, document_name, file_url, created_at").single();
522
541
  if (insertErr)
523
542
  return { success: false, error: insertErr.message };
543
+ if (urlData.publicUrl.endsWith('.pdf'))
544
+ await generateThumbnail(sb, record.id, urlData.publicUrl).catch(() => { });
524
545
  return { success: true, data: { id: record.id, name: record.document_name, type: docType, template: template.name, url: record.file_url, size: sizeBytes } };
525
546
  }
526
547
  case "list_stores": {
@@ -684,6 +705,16 @@ export async function handleDocuments(sb, args, storeId) {
684
705
  if (cannabinoidCfg && !mergedData.cannabinoids) {
685
706
  mergedData.cannabinoids = generateCannabinoidData(cannabinoidCfg, mergedConstants);
686
707
  }
708
+ // 6b. Flatten cannabinoid rows into top-level keys for calculation formulas
709
+ // e.g. { name: "D9-THC", percentWeight: 0.22 } → D9_THC: 0.22
710
+ if (Array.isArray(mergedData.cannabinoids)) {
711
+ for (const row of mergedData.cannabinoids) {
712
+ const key = row.name.replace(/-/g, "_").replace(/[^a-zA-Z0-9_]/g, "");
713
+ if (key && row.percentWeight !== undefined) {
714
+ mergedData[key] = row.percentWeight;
715
+ }
716
+ }
717
+ }
687
718
  // 7. Apply calculations
688
719
  mergedData = applyCalculations(tpl.calculations, mergedConstants, mergedData);
689
720
  // Also run per-row calculations if cannabinoids exist
@@ -715,6 +746,16 @@ export async function handleDocuments(sb, args, storeId) {
715
746
  residualSolvents: { status: "PASS" },
716
747
  };
717
748
  }
749
+ // 7b2. Auto-set test status flags for summary section
750
+ if (mergedData.cannabinoids && mergedData.cannabinoids.length > 0) {
751
+ mergedData.testsCannabinoids = true;
752
+ }
753
+ if (mergedData.moisture !== undefined && mergedData.moisture !== null) {
754
+ mergedData.testsMoisture = true;
755
+ }
756
+ if (mergedData.dateTested) {
757
+ mergedData.testsBatch = true;
758
+ }
718
759
  // 7c. Generate QR code for COA templates — links to Quantix COA landing page
719
760
  if (tpl.slug?.startsWith("cannabis-coa") && !mergedData.qrCodeDataUrl) {
720
761
  const productSlug = (mergedData.sampleName || "certificate")
@@ -843,6 +884,7 @@ export async function handleDocuments(sb, args, storeId) {
843
884
  }).select("id, document_name, file_url, created_at").single();
844
885
  if (insertErr)
845
886
  return { success: false, error: insertErr.message };
887
+ await generateThumbnail(sb, record.id, urlData.publicUrl).catch(() => { });
846
888
  return {
847
889
  success: true,
848
890
  data: {
@@ -907,32 +949,8 @@ export async function handleDocuments(sb, args, storeId) {
907
949
  };
908
950
  }
909
951
  case "list_clients":
910
- case "list_customers": {
911
- let query = sb.from("v_store_customers")
912
- .select("id, first_name, last_name, email, phone, loyalty_tier, total_orders, is_active, created_at")
913
- .eq("store_id", sid)
914
- .order("created_at", { ascending: false });
915
- if (args.query) {
916
- const term = `%${sanitizeFilterValue(String(args.query).trim())}%`;
917
- query = query.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term}`);
918
- }
919
- if (args.limit)
920
- query = query.limit(args.limit);
921
- const { data, error } = await query;
922
- if (error)
923
- return { success: false, error: error.message };
924
- return {
925
- success: true,
926
- data: {
927
- count: data?.length || 0,
928
- customers: (data || []).map(c => ({
929
- id: c.id, name: [c.first_name, c.last_name].filter(Boolean).join(" "),
930
- email: c.email, phone: c.phone, loyalty_tier: c.loyalty_tier,
931
- total_orders: c.total_orders, is_active: c.is_active,
932
- })),
933
- },
934
- };
935
- }
952
+ case "list_customers":
953
+ return { success: false, error: "Use the 'customers' tool instead. Clients are customers." };
936
954
  case "deliver_documents": {
937
955
  // Batch-assign documents to a customer and optionally send notification email
938
956
  const docIds = args.document_ids || (args.document_id ? [args.document_id] : []);
@@ -1016,6 +1034,6 @@ export async function handleDocuments(sb, args, storeId) {
1016
1034
  };
1017
1035
  }
1018
1036
  default:
1019
- return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, list_customers, deliver_documents, list_customer_documents` };
1037
+ return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, deliver_documents, list_customer_documents` };
1020
1038
  }
1021
1039
  }
@@ -1084,6 +1084,28 @@ export async function handleVoice(sb, args, storeId) {
1084
1084
  headers: { "xi-api-key": apiKey },
1085
1085
  });
1086
1086
  if (!resp.ok) {
1087
+ // If key lacks user_read scope, fall back to a partial status check
1088
+ if (resp.status === 401 || resp.status === 403) {
1089
+ const voicesResp = await fetchWithRetry(`${ELEVENLABS_BASE}/voices`, {
1090
+ headers: { "xi-api-key": apiKey },
1091
+ });
1092
+ const keyWorks = voicesResp.ok;
1093
+ const voiceCount = keyWorks ? ((await voicesResp.json()).voices?.length ?? 0) : 0;
1094
+ return {
1095
+ success: true,
1096
+ data: {
1097
+ tier: "unknown (API key missing user_read scope)",
1098
+ character_count: null,
1099
+ character_limit: null,
1100
+ characters_remaining: null,
1101
+ usage_percent: null,
1102
+ next_reset: null,
1103
+ key_valid: keyWorks,
1104
+ voice_count: voiceCount,
1105
+ note: "ElevenLabs API key is missing the 'user_read' permission. Regenerate the key at elevenlabs.io with full permissions to see usage stats. All generation features (speak, music, SFX) still work.",
1106
+ },
1107
+ };
1108
+ }
1087
1109
  const errText = await resp.text();
1088
1110
  return { success: false, error: `Usage error ${resp.status}: ${errText}` };
1089
1111
  }
@@ -8,7 +8,7 @@ import { randomUUID, timingSafeEqual, createHash } from "node:crypto";
8
8
  import Anthropic from "@anthropic-ai/sdk";
9
9
  import { createLogger } from "./lib/logger.js";
10
10
  const log = createLogger("server");
11
- import { getMaxOutputTokens, sanitizeError, } from "../shared/agent-core.js";
11
+ import { sanitizeError, resolveAgentLoopConfig, } from "../shared/agent-core.js";
12
12
  import { MODELS } from "../shared/constants.js";
13
13
  import { handleProxy } from "./proxy-handlers.js";
14
14
  import { handleNodeRoutes, setNodeAgentInvoker } from "./handlers/nodes.js";
@@ -939,8 +939,12 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
939
939
  clientContext: context, userId, userEmail, extendedTools: getExtendedToolsIndex(),
940
940
  });
941
941
  const anthropic = getAnthropicClient(agent);
942
- const ctxCfg = agent.context_config;
943
- const MAX_HISTORY_CHARS = ctxCfg?.max_history_chars || 400_000;
942
+ // Resolve all behavioral config from DB — single source of truth
943
+ const resolved = resolveAgentLoopConfig(agent, "sse");
944
+ if (resolved.defaultsUsed.length > 0) {
945
+ log.info({ agentId, defaults: resolved.defaultsUsed }, "agent config defaults applied");
946
+ }
947
+ const MAX_HISTORY_CHARS = resolved.maxHistoryChars;
944
948
  // Build user message — multi-modal if image attachments present
945
949
  let userContent;
946
950
  if (attachments?.length) {
@@ -978,7 +982,6 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
978
982
  // Client disconnect detection
979
983
  let clientDisconnected = false;
980
984
  req.on("close", () => { clientDisconnected = true; });
981
- const maxDurationMs = 15 * 60 * 1000;
982
985
  const startedAt = Date.now();
983
986
  const chatStartTime = Date.now();
984
987
  try {
@@ -990,9 +993,17 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
990
993
  messages,
991
994
  tools,
992
995
  extendedTools,
993
- maxTurns: agent.max_tool_calls || 10,
994
- temperature: agent.temperature ?? 0.7,
995
- maxTokens: getMaxOutputTokens(agentModel, agent.max_tokens),
996
+ // All behavioral knobs from resolved config — DB is single source of truth
997
+ maxTurns: resolved.maxTurns,
998
+ temperature: resolved.temperature,
999
+ maxTokens: resolved.maxTokens,
1000
+ maxConcurrentTools: resolved.maxConcurrentTools,
1001
+ enableDelegation: resolved.enableDelegation,
1002
+ enableModelRouting: resolved.enableModelRouting,
1003
+ contextOverrides: resolved.contextOverrides,
1004
+ subagentMaxTokens: resolved.subagentMaxTokens,
1005
+ subagentMaxTurns: resolved.subagentMaxTurns,
1006
+ subagentTemperature: resolved.subagentTemperature,
996
1007
  storeId,
997
1008
  traceId,
998
1009
  userId,
@@ -1022,7 +1033,7 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
1022
1033
  },
1023
1034
  clientDisconnected: { get value() { return clientDisconnected; } },
1024
1035
  startedAt,
1025
- maxDurationMs,
1036
+ maxDurationMs: resolved.maxDurationMs,
1026
1037
  });
1027
1038
  // Send usage SSE
1028
1039
  sendSSE(res, {
@@ -2344,16 +2355,14 @@ setAgentExecutor(async (supabase, agentId, prompt, storeId, maxTurns = 5, onToke
2344
2355
  const { rows: userToolRows, defs: userToolDefs } = await loadUserTools(supabase, storeId);
2345
2356
  const tools = getToolsForAgent(agent, allTools, userToolDefs);
2346
2357
  const agentModel = agent.model || MODELS.SONNET;
2347
- // Sanitize the DB-stored agent system prompt to prevent injection attacks
2348
- const rawWorkflowPrompt = agent.system_prompt || "You are a helpful assistant.";
2349
- let systemPrompt = sanitizeAndLog(rawWorkflowPrompt, "workflowAgentExecutor", { agentId });
2350
- systemPrompt += `\n\nYou are operating for store_id: ${storeId}. Always include this in tool calls that require it.`;
2351
- if (!agent.can_modify)
2352
- systemPrompt += "\n\nIMPORTANT: You have read-only access.";
2353
- if (agent.tone && agent.tone !== "professional")
2354
- systemPrompt += `\n\nTone: ${agent.tone}`;
2355
- if (agent.verbosity === "concise")
2356
- systemPrompt += "\n\nBe concise.";
2358
+ // Resolve all behavioral config from DB workflow maxTurns caps agent config
2359
+ const resolved = resolveAgentLoopConfig(agent, "workflow", maxTurns);
2360
+ if (resolved.defaultsUsed.length > 0) {
2361
+ log.info({ agentId, callPath: "workflow", defaults: resolved.defaultsUsed }, "workflow agent config defaults applied");
2362
+ }
2363
+ // Build full system prompt shared helper ensures workflow gets same context as SSE/channel
2364
+ // (previously hand-built, missing location/customer context)
2365
+ const { systemPrompt } = await buildAgentSystemPrompt(supabase, agent, storeId, prompt, tools);
2357
2366
  try {
2358
2367
  const result = await runServerAgentLoop({
2359
2368
  anthropic: getAnthropicClient(agent),
@@ -2362,8 +2371,17 @@ setAgentExecutor(async (supabase, agentId, prompt, storeId, maxTurns = 5, onToke
2362
2371
  systemPrompt,
2363
2372
  messages: [{ role: "user", content: prompt }],
2364
2373
  tools,
2365
- maxTurns,
2366
- temperature: agent.temperature ?? 0.7,
2374
+ // All behavioral knobs from resolved config — DB is single source of truth
2375
+ maxTurns: resolved.maxTurns,
2376
+ temperature: resolved.temperature,
2377
+ maxTokens: resolved.maxTokens,
2378
+ maxConcurrentTools: resolved.maxConcurrentTools,
2379
+ enableDelegation: resolved.enableDelegation,
2380
+ enableModelRouting: resolved.enableModelRouting,
2381
+ contextOverrides: resolved.contextOverrides,
2382
+ subagentMaxTokens: resolved.subagentMaxTokens,
2383
+ subagentMaxTurns: resolved.subagentMaxTurns,
2384
+ subagentTemperature: resolved.subagentTemperature,
2367
2385
  storeId,
2368
2386
  source: "workflow_agent",
2369
2387
  agentId,
@@ -2376,7 +2394,7 @@ setAgentExecutor(async (supabase, agentId, prompt, storeId, maxTurns = 5, onToke
2376
2394
  },
2377
2395
  enableStreaming: !!onToken,
2378
2396
  onText: onToken || undefined,
2379
- maxDurationMs: 2 * 60 * 1000,
2397
+ maxDurationMs: resolved.maxDurationMs,
2380
2398
  });
2381
2399
  return { success: true, response: result.finalText || "(no response)" };
2382
2400
  }
@@ -2403,6 +2421,11 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
2403
2421
  const { rows: userToolRows, defs: userToolDefs } = await loadUserTools(supabase, storeId);
2404
2422
  const tools = getToolsForAgent(agent, coreTools, userToolDefs);
2405
2423
  const agentModel = agent.model || MODELS.SONNET;
2424
+ // Resolve all behavioral config from DB — channel has 15-turn safety cap built in
2425
+ const resolved = resolveAgentLoopConfig(agent, "channel");
2426
+ if (resolved.defaultsUsed.length > 0) {
2427
+ log.info({ agentId, callPath: "channel", defaults: resolved.defaultsUsed }, "channel agent config defaults applied");
2428
+ }
2406
2429
  // Build system prompt — shared helper, identical to SSE chat
2407
2430
  const { systemPrompt, dynamicContext } = await buildAgentSystemPrompt(supabase, agent, storeId, message, tools, { senderContext, extendedTools: getExtendedToolsIndex() });
2408
2431
  // Update ai_conversations with agent_id (fire-and-forget)
@@ -2410,8 +2433,7 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
2410
2433
  Promise.resolve(supabase.from("ai_conversations").update({ agent_id: agentId }).eq("id", conversationId).is("agent_id", null)).catch(() => { });
2411
2434
  }
2412
2435
  // Load conversation history with size-based compaction (same as SSE chat)
2413
- const ctxCfg = agent.context_config;
2414
- const MAX_HISTORY_CHARS = ctxCfg?.max_history_chars || 400_000;
2436
+ const MAX_HISTORY_CHARS = resolved.maxHistoryChars;
2415
2437
  let loadedHistory = [];
2416
2438
  if (conversationId) {
2417
2439
  try {
@@ -2448,8 +2470,17 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
2448
2470
  messages,
2449
2471
  tools,
2450
2472
  extendedTools,
2451
- maxTurns: Math.min(agent.max_tool_calls || 10, 15),
2452
- temperature: agent.temperature ?? 0.7,
2473
+ // All behavioral knobs from resolved config — DB is single source of truth
2474
+ maxTurns: resolved.maxTurns,
2475
+ temperature: resolved.temperature,
2476
+ maxTokens: resolved.maxTokens,
2477
+ maxConcurrentTools: resolved.maxConcurrentTools,
2478
+ enableDelegation: resolved.enableDelegation,
2479
+ enableModelRouting: resolved.enableModelRouting,
2480
+ contextOverrides: resolved.contextOverrides,
2481
+ subagentMaxTokens: resolved.subagentMaxTokens,
2482
+ subagentMaxTurns: resolved.subagentMaxTurns,
2483
+ subagentTemperature: resolved.subagentTemperature,
2453
2484
  storeId,
2454
2485
  source: "channel_agent",
2455
2486
  agentId,
@@ -2466,7 +2497,7 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
2466
2497
  return executeTool(supabase, toolName, toolArgs, storeId, traceId, senderUserId, senderUserLabel, "channel_agent", conversationId, userToolRows, agentId, undefined, true);
2467
2498
  },
2468
2499
  enableStreaming: false,
2469
- maxDurationMs: 2 * 60 * 1000,
2500
+ maxDurationMs: resolved.maxDurationMs,
2470
2501
  });
2471
2502
  // Persist everything — shared helper, identical to SSE chat
2472
2503
  await persistAgentTurn(supabase, agent, {
@@ -34,9 +34,9 @@ class ClickHouseClient {
34
34
  return `https://${this.host}:8443`;
35
35
  }
36
36
  get authHeaders() {
37
+ const credentials = Buffer.from(`${this.user}:${this.password}`).toString("base64");
37
38
  return {
38
- "X-ClickHouse-User": this.user,
39
- "X-ClickHouse-Key": this.password,
39
+ Authorization: `Basic ${credentials}`,
40
40
  "X-ClickHouse-Database": this.database,
41
41
  };
42
42
  }
@@ -75,7 +75,7 @@ interface Calculation {
75
75
  formula: string;
76
76
  decimals?: number;
77
77
  }
78
- export declare function applyCalculations(calculations: Calculation[] | undefined | null, constants: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
78
+ export declare function applyCalculations(calculations: Calculation[] | Record<string, string> | undefined | null, constants: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
79
79
  interface ValidationRule {
80
80
  name: string;
81
81
  formula: string;
@@ -225,7 +225,7 @@ function tokenize(expr) {
225
225
  i++;
226
226
  continue;
227
227
  }
228
- if (/[0-9.]/.test(ch)) {
228
+ if (/[0-9]/.test(ch) || (ch === "." && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]))) {
229
229
  let num = "";
230
230
  while (i < expr.length && /[0-9.]/.test(expr[i])) {
231
231
  num += expr[i];
@@ -391,8 +391,22 @@ function safeEval(expr, ctx) {
391
391
  const parser = new ExprParser(tokens, ctx);
392
392
  return parser.parse();
393
393
  }
394
+ function normalizeCalculations(raw) {
395
+ if (!raw)
396
+ return [];
397
+ if (Array.isArray(raw))
398
+ return raw;
399
+ if (typeof raw === "object") {
400
+ return Object.entries(raw).map(([field, formula]) => ({
401
+ field,
402
+ formula: String(formula),
403
+ }));
404
+ }
405
+ return [];
406
+ }
394
407
  export function applyCalculations(calculations, constants, data) {
395
- if (!calculations || !Array.isArray(calculations))
408
+ const normalized = normalizeCalculations(calculations);
409
+ if (!normalized.length)
396
410
  return data;
397
411
  const result = { ...data };
398
412
  // Flatten constants into context
@@ -405,7 +419,7 @@ export function applyCalculations(calculations, constants, data) {
405
419
  // Multi-pass (max 3) for inter-formula dependencies
406
420
  for (let pass = 0; pass < 3; pass++) {
407
421
  let changed = false;
408
- for (const calc of calculations) {
422
+ for (const calc of normalized) {
409
423
  try {
410
424
  const val = safeEval(calc.formula, ctx);
411
425
  const rounded = parseFloat(val.toFixed(calc.decimals ?? 3));
@@ -433,7 +447,7 @@ function tokenizeBool(expr) {
433
447
  i++;
434
448
  continue;
435
449
  }
436
- if (/[0-9.]/.test(ch)) {
450
+ if (/[0-9]/.test(ch) || (ch === "." && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]))) {
437
451
  let num = "";
438
452
  while (i < expr.length && /[0-9.]/.test(expr[i])) {
439
453
  num += expr[i];
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type Anthropic from "@anthropic-ai/sdk";
12
12
  import type { SupabaseClient } from "@supabase/supabase-js";
13
+ import { type ContextManagementOverrides } from "../../shared/agent-core.js";
13
14
  import type { ToolChoice } from "../../shared/agent-core.js";
14
15
  import type { CitationBlock } from "../../shared/types.js";
15
16
  import type { ToolProgressCallback } from "../tool-router.js";
@@ -51,6 +52,7 @@ export interface ServerAgentLoopOptions {
51
52
  enableDelegation?: boolean;
52
53
  enablePromptCaching?: boolean;
53
54
  enableStreaming?: boolean;
55
+ enableModelRouting?: boolean;
54
56
  maxConcurrentTools?: number;
55
57
  documents?: any[];
56
58
  onText?: (text: string) => void;
@@ -58,6 +60,9 @@ export interface ServerAgentLoopOptions {
58
60
  onToolResult?: (name: string, success: boolean, result: unknown) => void;
59
61
  onCitation?: (citation: CitationBlock) => void;
60
62
  onSubagentProgress?: SubagentProgressCallback;
63
+ subagentMaxTokens?: number;
64
+ subagentMaxTurns?: number;
65
+ subagentTemperature?: number;
61
66
  maxCostUsd?: number;
62
67
  clientDisconnected?: {
63
68
  value: boolean;
@@ -65,6 +70,7 @@ export interface ServerAgentLoopOptions {
65
70
  startedAt?: number;
66
71
  maxDurationMs?: number;
67
72
  requiredCapabilities?: RequestCapabilityRequirements;
73
+ contextOverrides?: ContextManagementOverrides;
68
74
  }
69
75
  export interface TurnMetrics {
70
76
  turn: number;