whale-code 6.5.4 → 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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Agent Config Loader — fetches agent settings from ai_agent_config (Supabase).
3
+ *
4
+ * Single source of truth: the DB record IS the config.
5
+ * Nothing is hardcoded — all behavioral knobs come from context_config JSONB.
6
+ * Cached for the session lifetime (5 min TTL) to avoid hammering the DB.
7
+ */
8
+ import type { AgentContextConfig } from "../../shared/agent-core.js";
9
+ export interface CLIAgentConfig {
10
+ id: string;
11
+ name: string;
12
+ model: string;
13
+ max_tokens: number;
14
+ max_tool_calls: number;
15
+ temperature: number;
16
+ context_config: AgentContextConfig | null;
17
+ }
18
+ /**
19
+ * Fetch the Whale Code agent's config from ai_agent_config.
20
+ * Uses the defaultAgentId from CLI config, or falls back to the known Whale Code UUID.
21
+ * Cached for 5 minutes — call freely from hot paths.
22
+ */
23
+ export declare function loadCLIAgentConfig(): Promise<CLIAgentConfig | null>;
24
+ /** Clear the cache (e.g., on /clear or session reset) */
25
+ export declare function resetAgentConfigCache(): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Agent Config Loader — fetches agent settings from ai_agent_config (Supabase).
3
+ *
4
+ * Single source of truth: the DB record IS the config.
5
+ * Nothing is hardcoded — all behavioral knobs come from context_config JSONB.
6
+ * Cached for the session lifetime (5 min TTL) to avoid hammering the DB.
7
+ */
8
+ import { createClient } from "@supabase/supabase-js";
9
+ import { resolveConfig } from "./config-store.js";
10
+ // ============================================================================
11
+ // CACHE
12
+ // ============================================================================
13
+ let cachedConfig = null;
14
+ let cacheTimestamp = 0;
15
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
16
+ /**
17
+ * Fetch the Whale Code agent's config from ai_agent_config.
18
+ * Uses the defaultAgentId from CLI config, or falls back to the known Whale Code UUID.
19
+ * Cached for 5 minutes — call freely from hot paths.
20
+ */
21
+ export async function loadCLIAgentConfig() {
22
+ if (cachedConfig && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
23
+ return cachedConfig;
24
+ }
25
+ try {
26
+ const { supabaseUrl, supabaseKey, defaultAgentId } = resolveConfig();
27
+ if (!supabaseUrl || !supabaseKey)
28
+ return cachedConfig;
29
+ const agentId = defaultAgentId || "72d7aaa8-2e8b-48a0-a38d-f332f69600bb";
30
+ const supabase = createClient(supabaseUrl, supabaseKey);
31
+ const { data, error } = await supabase
32
+ .from("ai_agent_config")
33
+ .select("id, name, model, max_tokens, max_tool_calls, temperature, context_config")
34
+ .eq("id", agentId)
35
+ .single();
36
+ if (error || !data) {
37
+ // Silently fall back to cached config on error — never block the CLI
38
+ return cachedConfig;
39
+ }
40
+ cachedConfig = {
41
+ id: data.id,
42
+ name: data.name,
43
+ model: data.model,
44
+ max_tokens: data.max_tokens,
45
+ max_tool_calls: data.max_tool_calls,
46
+ temperature: data.temperature,
47
+ context_config: data.context_config,
48
+ };
49
+ cacheTimestamp = Date.now();
50
+ return cachedConfig;
51
+ }
52
+ catch {
53
+ // Network failure, auth expired, etc. — never crash the CLI
54
+ return cachedConfig;
55
+ }
56
+ }
57
+ /** Clear the cache (e.g., on /clear or session reset) */
58
+ export function resetAgentConfigCache() {
59
+ cachedConfig = null;
60
+ cacheTimestamp = 0;
61
+ }
@@ -26,7 +26,8 @@ import { captureError, addBreadcrumb } from "./error-logger.js";
26
26
  import { setGlobalEmitter, clearGlobalEmitter, } from "./agent-events.js";
27
27
  import { mcpClientManager } from "./mcp-client.js";
28
28
  import { loadHooks, runBeforeToolHook, runAfterToolHook, runSessionHook } from "./hooks.js";
29
- import { LoopDetector, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, getCompactionConfig, DEFAULT_SESSION_COST_BUDGET_USD, emitCostWarningIfNeeded } from "../../shared/agent-core.js";
29
+ import { LoopDetector, COMPACTION_TOTAL_BUDGET, getCompactionConfig, getContextManagement, DEFAULT_SESSION_COST_BUDGET_USD, emitCostWarningIfNeeded, resolveAgentLoopConfig, AGENT_DEFAULTS } from "../../shared/agent-core.js";
30
+ import { loadCLIAgentConfig, resetAgentConfigCache } from "./agent-config.js";
30
31
  import { parseSSEStream, processStreamWithCallbacks, collectStreamResult } from "../../shared/sse-parser.js";
31
32
  import { callServerProxy, callTranscribe, buildAPIRequest, buildSystemBlocks, prepareWithCaching, trimGeminiContext, trimOpenAIContext, requestProviderCompaction } from "../../shared/api-client.js";
32
33
  import { getProvider, MODELS } from "../../shared/constants.js";
@@ -88,10 +89,13 @@ export function resetSessionState() {
88
89
  resetGitContext();
89
90
  resetClaudeMdCache();
90
91
  clearReadCache();
92
+ resetAgentConfigCache();
91
93
  }
92
94
  /** CLI-only: loop detector — persists session error state across turns (reset by resetSessionState) */
93
95
  let sessionLoopDetector = null;
94
- const MAX_TURNS = 200; // Match Claude Code effectively unlimited within a session
96
+ // CLI max turns high ceiling since user controls via Ctrl+C.
97
+ // DB agent config maxTurns is used when available, this is the absolute safety cap.
98
+ const CLI_MAX_TURNS_CAP = 200;
95
99
  // ============================================================================
96
100
  // SHELL OUTPUT SUMMARIZATION
97
101
  // ============================================================================
@@ -231,7 +235,8 @@ export async function runAgentLoop(opts) {
231
235
  if (emitter) {
232
236
  setGlobalEmitter(emitter);
233
237
  }
234
- const effectiveMaxTurns = opts.maxTurns || MAX_TURNS;
238
+ // effectiveMaxTurns resolved after DB config fetch below
239
+ let effectiveMaxTurns = opts.maxTurns || CLI_MAX_TURNS_CAP;
235
240
  // Load hooks from project and user config
236
241
  const hooksCwd = opts.cwd || process.cwd();
237
242
  const hooks = loadHooks(hooksCwd);
@@ -308,8 +313,27 @@ export async function runAgentLoop(opts) {
308
313
  let sessionCostUsd = 0;
309
314
  let compactionCount = 0;
310
315
  const costWarningsEmitted = new Set();
311
- const effectiveBudget = opts.maxBudgetUsd ?? DEFAULT_SESSION_COST_BUDGET_USD;
312
316
  const activeModel = getModel();
317
+ // Fetch agent config from DB — single source of truth, nothing hardcoded.
318
+ // Falls back to AGENT_DEFAULTS only if DB is unreachable (never blocks).
319
+ const dbAgent = await loadCLIAgentConfig();
320
+ const resolved = dbAgent
321
+ ? resolveAgentLoopConfig(dbAgent, "sse")
322
+ : null;
323
+ const effectiveBudget = opts.maxBudgetUsd
324
+ ?? resolved?.contextOverrides.session_cost_budget_usd
325
+ ?? DEFAULT_SESSION_COST_BUDGET_USD;
326
+ // Compaction settings from DB via getContextManagement — pass DB overrides so
327
+ // the trigger/budget values match what the Anthropic API actually uses.
328
+ const ctxOverrides = resolved?.contextOverrides;
329
+ const ctxMgmt = getContextManagement(activeModel, ctxOverrides);
330
+ const compactEdit = ctxMgmt.config.edits.find((e) => e.type === "compact_20260112");
331
+ const effectiveCompactionTrigger = compactEdit?.trigger?.value ?? AGENT_DEFAULTS.compactionTriggerTokens;
332
+ const effectiveCompactionBudget = ctxOverrides?.compaction_total_budget ?? COMPACTION_TOTAL_BUDGET;
333
+ // Override maxTurns from DB config (if not explicitly set by caller)
334
+ if (!opts.maxTurns && resolved) {
335
+ effectiveMaxTurns = resolved.maxTurns;
336
+ }
313
337
  // Tool executor — routes to interactive, local, server, or MCP tools.
314
338
  // Wraps execution with before/after hooks when hooks are loaded.
315
339
  const INTERACTIVE_TOOL_NAMES = new Set(INTERACTIVE_TOOL_DEFINITIONS.map(t => t.name));
@@ -375,9 +399,6 @@ export async function runAgentLoop(opts) {
375
399
  }
376
400
  return result;
377
401
  };
378
- // Deprecated: Anthropic context_management handles limits via clear_tool_uses + compaction.
379
- // tool-dispatch.ts uses SAFETY_MAX_CHARS (500K) — this value is ignored.
380
- const maxResultChars = undefined;
381
402
  try {
382
403
  for (let iteration = 0; iteration < effectiveMaxTurns; iteration++) {
383
404
  if (abortSignal?.aborted) {
@@ -401,6 +422,7 @@ export async function runAgentLoop(opts) {
401
422
  model: currentModel,
402
423
  contextProfile: "main",
403
424
  thinkingEnabled: opts.thinking,
425
+ contextOverrides: ctxOverrides,
404
426
  });
405
427
  // Prepare with prompt caching
406
428
  let { tools: cachedTools, messages: cachedMessages } = prepareWithCaching(tools, messages);
@@ -557,7 +579,7 @@ export async function runAgentLoop(opts) {
557
579
  compactionCount++;
558
580
  logSpan({ action: "chat.compaction_pause", durationMs: Date.now() - apiStart, context: turnCtx, storeId: storeId || undefined, details: { compaction_count: compactionCount, messages_before: messages.length } });
559
581
  // Budget enforcement
560
- if (compactionCount * COMPACTION_TRIGGER_TOKENS >= COMPACTION_TOTAL_BUDGET) {
582
+ if (compactionCount * effectiveCompactionTrigger >= effectiveCompactionBudget) {
561
583
  const budgetMsg = "\n[Context budget reached — wrapping up.]";
562
584
  if (emitter) {
563
585
  emitter.emitText(budgetMsg);
@@ -605,7 +627,6 @@ export async function runAgentLoop(opts) {
605
627
  const { results: toolResults, bailOut, bailMessage } = await dispatchTools(result.toolUseBlocks, toolExecutor, {
606
628
  loopDetector,
607
629
  maxConcurrent: 7,
608
- maxResultChars,
609
630
  onStart: (name, input) => {
610
631
  callbacks.onToolStart(name, input);
611
632
  },
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Error Logger — fire-and-forget error capture to error_events table
2
+ * Error Logger — fire-and-forget error capture to ClickHouse error_events table
3
3
  *
4
- * Sentry replacement built on our own Supabase infrastructure.
4
+ * Sentry replacement built on our own ClickHouse infrastructure.
5
5
  * Mirrors the Swift WhaleErrorLogger: same fingerprinting, same table, same schema.
6
- * Reuses the telemetry.ts Supabase client pattern.
7
6
  */
8
7
  export interface ErrorLoggerConfig {
9
8
  serviceName: string;
@@ -1,16 +1,12 @@
1
1
  /**
2
- * Error Logger — fire-and-forget error capture to error_events table
2
+ * Error Logger — fire-and-forget error capture to ClickHouse error_events table
3
3
  *
4
- * Sentry replacement built on our own Supabase infrastructure.
4
+ * Sentry replacement built on our own ClickHouse infrastructure.
5
5
  * Mirrors the Swift WhaleErrorLogger: same fingerprinting, same table, same schema.
6
- * Reuses the telemetry.ts Supabase client pattern.
7
6
  */
8
- import { createClient } from "@supabase/supabase-js";
9
7
  import { createRequire } from "module";
10
8
  import { createHash } from "crypto";
11
9
  import os from "os";
12
- import { resolveConfig } from "./config-store.js";
13
- import { getValidToken, createAuthenticatedClient } from "./auth-service.js";
14
10
  const require = createRequire(import.meta.url);
15
11
  const PKG_VERSION = require("../../../package.json").version;
16
12
  // ============================================================================
@@ -24,7 +20,6 @@ let config = {
24
20
  maxBreadcrumbs: 25,
25
21
  };
26
22
  let breadcrumbs = [];
27
- let supabaseClient = null;
28
23
  // User context (set externally)
29
24
  let currentUserId;
30
25
  let currentUserEmail;
@@ -98,10 +93,12 @@ export function captureMessage(message, severity = "info", extra) {
98
93
  // INTERNAL
99
94
  // ============================================================================
100
95
  async function _captureError(opts) {
101
- const client = await getClient();
102
- if (!client) {
96
+ const host = process.env.CLICKHOUSE_HOST || "jmp0o2f6so.us-east-1.aws.clickhouse.cloud";
97
+ const user = process.env.CLICKHOUSE_USER || "default";
98
+ const password = process.env.CLICKHOUSE_PASSWORD;
99
+ if (!host || !password) {
103
100
  if (process.env.DEBUG_TELEMETRY) {
104
- process.stderr.write(`[error-logger] no client available\n`);
101
+ process.stderr.write(`[error-logger] ClickHouse not configured\n`);
105
102
  }
106
103
  return;
107
104
  }
@@ -114,61 +111,55 @@ async function _captureError(opts) {
114
111
  ? `${opts.file}:${opts.function}`
115
112
  : extractSourceFromStack(stackTrace);
116
113
  const fingerprint = computeFingerprint(errorType, normalizedMessage, sourceLocation);
117
- // Parse source from stack if not provided
118
114
  const parsedSource = parseSourceFromStack(stackTrace);
119
115
  const event = {
120
116
  fingerprint,
121
117
  severity: opts.severity || "error",
122
118
  error_type: errorType,
123
- error_message: errorMessage,
124
- error_code: opts.errorCode,
125
- source_file: opts.file || parsedSource.file,
126
- source_line: opts.line || parsedSource.line,
127
- source_function: opts.function || parsedSource.function,
128
- stack_trace: stackTrace,
129
- stack_frames: stackTrace ? parseStackFrames(stackTrace) : undefined,
130
- breadcrumbs: [...breadcrumbs],
119
+ error_message: errorMessage.slice(0, 4096),
120
+ error_code: opts.errorCode || "",
121
+ source_file: opts.file || parsedSource.file || "",
122
+ source_line: opts.line || parsedSource.line || 0,
123
+ source_function: opts.function || parsedSource.function || "",
124
+ stack_trace: stackTrace || "",
131
125
  platform: config.platform || "typescript",
132
126
  service_name: config.serviceName,
133
- service_version: config.serviceVersion,
127
+ service_version: config.serviceVersion || "",
134
128
  environment: config.environment || "production",
135
- device_info: collectDeviceInfo(),
136
- runtime_info: collectRuntimeInfo(),
137
- store_id: opts.storeId || currentStoreId,
138
- user_id: opts.userId || currentUserId,
139
- user_email: opts.userEmail || currentUserEmail,
140
- trace_id: opts.traceId,
141
- span_id: opts.spanId,
142
- tags: opts.tags || {},
143
- extra: opts.extra || {},
129
+ device_info: JSON.stringify(collectDeviceInfo()),
130
+ runtime_info: JSON.stringify(collectRuntimeInfo()),
131
+ store_id: opts.storeId || currentStoreId || "",
132
+ user_id: opts.userId || currentUserId || "",
133
+ user_email: opts.userEmail || currentUserEmail || "",
134
+ trace_id: opts.traceId || "",
135
+ span_id: opts.spanId || "",
136
+ request_id: "",
137
+ tags: JSON.stringify(opts.tags || {}),
138
+ extra: JSON.stringify(opts.extra || {}),
139
+ breadcrumbs: JSON.stringify(breadcrumbs),
144
140
  occurred_at: new Date().toISOString(),
145
141
  };
146
- const { error: dbError } = await client.from("error_events").insert(event);
147
- if (dbError && process.env.DEBUG_TELEMETRY) {
148
- process.stderr.write(`[error-logger] db error: ${dbError.message}\n`);
149
- }
150
- }
151
- // ============================================================================
152
- // SUPABASE CLIENT (lazy init, same pattern as telemetry.ts)
153
- // ============================================================================
154
- async function getClient() {
155
- if (supabaseClient)
156
- return supabaseClient;
157
- const cfg = resolveConfig();
158
- // Prefer service role key
159
- if (cfg.supabaseUrl && cfg.supabaseKey) {
160
- supabaseClient = createClient(cfg.supabaseUrl, cfg.supabaseKey, {
161
- auth: { persistSession: false, autoRefreshToken: false },
142
+ try {
143
+ const credentials = Buffer.from(`${user}:${password}`).toString("base64");
144
+ const url = `https://${host}:8443/?query=${encodeURIComponent("INSERT INTO error_events FORMAT JSONEachRow")}`;
145
+ const response = await fetch(url, {
146
+ method: "POST",
147
+ headers: {
148
+ Authorization: `Basic ${credentials}`,
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify(event),
162
152
  });
163
- return supabaseClient;
153
+ if (!response.ok && process.env.DEBUG_TELEMETRY) {
154
+ const text = await response.text();
155
+ process.stderr.write(`[error-logger] ClickHouse ${response.status}: ${text.slice(0, 200)}\n`);
156
+ }
164
157
  }
165
- // Fallback: user JWT
166
- const token = await getValidToken();
167
- if (token) {
168
- supabaseClient = createAuthenticatedClient(token);
169
- return supabaseClient;
158
+ catch (err) {
159
+ if (process.env.DEBUG_TELEMETRY) {
160
+ process.stderr.write(`[error-logger] insert failed: ${err.message}\n`);
161
+ }
170
162
  }
171
- return null;
172
163
  }
173
164
  // ============================================================================
174
165
  // FINGERPRINTING (identical algorithm to Swift)
@@ -11,7 +11,8 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } fr
11
11
  import { join } from "path";
12
12
  import { homedir, tmpdir } from "os";
13
13
  import { LOCAL_TOOL_DEFINITIONS, } from "./local-tools.js";
14
- import { LoopDetector, estimateCostUsd, demoteSubagentModel } from "../../shared/agent-core.js";
14
+ import { LoopDetector, estimateCostUsd, demoteSubagentModel, AGENT_DEFAULTS } from "../../shared/agent-core.js";
15
+ import { loadCLIAgentConfig } from "./agent-config.js";
15
16
  import { MODEL_MAP } from "../../shared/constants.js";
16
17
  import { loadServerToolDefinitions, } from "./server-tools.js";
17
18
  import { logSpan, generateSpanId, generateTraceId } from "./telemetry.js";
@@ -23,10 +24,9 @@ import { callAgentAPI, executeToolBlocks, extractTextBlocks, extractToolUseBlock
23
24
  // CONSTANTS
24
25
  // ============================================================================
25
26
  const AGENTS_DIR = join(homedir(), ".swagmanager", "agents");
26
- // Claude Code pattern: subagents should be discrete tasks, not open-ended
27
- // Low turn limit prevents infinite loops and forces focused execution
28
- const MAX_TURNS = 8;
29
- const SUBAGENT_MAX_OUTPUT_TOKENS = 32_000; // Claude Code parity — subagents get 32K output
27
+ // Subagent defaults overridden by DB config (ai_agent_config.context_config) when available.
28
+ // These are safety caps only; actual values come from AGENT_DEFAULTS or the resolved config.
29
+ const SUBAGENT_MAX_TURNS_CAP = 50; // Absolute safety cap
30
30
  // ============================================================================
31
31
  // AGENT PROMPTS — specialized per type
32
32
  // ============================================================================
@@ -289,7 +289,11 @@ export async function runSubagent(options) {
289
289
  const cwd = process.cwd();
290
290
  const systemPrompt = buildAgentPrompt(subagent_type, cwd);
291
291
  const startTime = Date.now();
292
- const effectiveMaxTurns = max_turns ? Math.max(1, Math.min(50, max_turns)) : MAX_TURNS;
292
+ // DB config caller override AGENT_DEFAULTS fallback
293
+ const dbAgent = await loadCLIAgentConfig();
294
+ const dbMaxTurns = dbAgent?.context_config?.subagent_max_turns ?? AGENT_DEFAULTS.subagentMaxTurns;
295
+ const dbMaxTokens = dbAgent?.context_config?.subagent_max_tokens ?? AGENT_DEFAULTS.subagentMaxTokens;
296
+ const effectiveMaxTurns = max_turns ? Math.max(1, Math.min(SUBAGENT_MAX_TURNS_CAP, max_turns)) : dbMaxTurns;
293
297
  // Extract short description from prompt (first sentence or 60 chars)
294
298
  const descMatch = prompt.match(/^[^.!?\n]+/);
295
299
  const shortDescription = name || (descMatch
@@ -369,7 +373,7 @@ export async function runSubagent(options) {
369
373
  messages: state.messages,
370
374
  tools,
371
375
  thinkingEnabled: false,
372
- maxOutputTokens: SUBAGENT_MAX_OUTPUT_TOKENS,
376
+ maxOutputTokens: dbMaxTokens,
373
377
  cacheLastTool: true,
374
378
  });
375
379
  lastError = null;
@@ -10,7 +10,8 @@
10
10
  import { Worker, parentPort, workerData, isMainThread } from "worker_threads";
11
11
  import { fileURLToPath } from "url";
12
12
  import { loadTeam, claimTask, completeTask, failTask, getAvailableTasks, sendMessage, getUnreadMessages, markMessagesRead, updateTeammate, } from "./team-state.js";
13
- import { LoopDetector, estimateCostUsd } from "../../shared/agent-core.js";
13
+ import { LoopDetector, estimateCostUsd, AGENT_DEFAULTS } from "../../shared/agent-core.js";
14
+ import { loadCLIAgentConfig } from "./agent-config.js";
14
15
  import { MODEL_MAP } from "../../shared/constants.js";
15
16
  import { LOCAL_TOOL_DEFINITIONS, } from "./local-tools.js";
16
17
  import { loadServerToolDefinitions, } from "./server-tools.js";
@@ -20,12 +21,23 @@ import { callAgentAPI, executeToolBlocks, extractTextBlocks, extractToolUseBlock
20
21
  // ============================================================================
21
22
  // CONSTANTS
22
23
  // ============================================================================
23
- const MAX_TURNS_PER_TASK = 12; // More turns than subagent since tasks are larger
24
- const MAX_OUTPUT_TOKENS = 16384;
25
- const MAX_TEAMMATE_TURNS = 50; // Outer loop safety: max task claim cycles
26
- const MAX_TEAMMATE_DURATION_MS = 10 * 60 * 1000; // 10 minute hard timeout
27
- const API_TIMEOUT_MS = 90_000; // 90s timeout on proxy/API fetch calls
28
- const TOOL_TIMEOUT_MS = 60_000; // 60s timeout on individual tool execution
24
+ // Teammate constants behavioral defaults from DB, infrastructure timeouts hardcoded (acceptable)
25
+ let TEAMMATE_MAX_TURNS_PER_TASK = 12;
26
+ let TEAMMATE_MAX_OUTPUT_TOKENS = 16384;
27
+ let TEAMMATE_MAX_TOTAL_TURNS = 50;
28
+ let TEAMMATE_MAX_DURATION_MS = AGENT_DEFAULTS.maxDurationMs;
29
+ const API_TIMEOUT_MS = 90_000; // 90s timeout on proxy/API fetch calls (infrastructure)
30
+ const TOOL_TIMEOUT_MS = 60_000; // 60s timeout on individual tool execution (infrastructure)
31
+ // Lazy init from DB — called once per teammate spawn
32
+ async function resolveTeammateConfig() {
33
+ const dbAgent = await loadCLIAgentConfig();
34
+ if (!dbAgent?.context_config)
35
+ return;
36
+ const cc = dbAgent.context_config;
37
+ TEAMMATE_MAX_TURNS_PER_TASK = cc.subagent_max_turns ?? TEAMMATE_MAX_TURNS_PER_TASK;
38
+ TEAMMATE_MAX_OUTPUT_TOKENS = cc.subagent_max_tokens ?? TEAMMATE_MAX_OUTPUT_TOKENS;
39
+ TEAMMATE_MAX_DURATION_MS = cc.max_duration_ms ?? TEAMMATE_MAX_DURATION_MS;
40
+ }
29
41
  // ============================================================================
30
42
  // TEAMMATE SYSTEM PROMPT
31
43
  // ============================================================================
@@ -221,7 +233,7 @@ async function callAPI(modelId, systemPrompt, messages, tools, thinkingEnabled =
221
233
  messages,
222
234
  tools,
223
235
  thinkingEnabled,
224
- maxOutputTokens: MAX_OUTPUT_TOKENS,
236
+ maxOutputTokens: TEAMMATE_MAX_OUTPUT_TOKENS,
225
237
  timeoutMs: API_TIMEOUT_MS,
226
238
  });
227
239
  }
@@ -232,6 +244,8 @@ async function runTeammateLoop(data) {
232
244
  const { teamId, teammateId, teammateName, model, cwd, parentConversationId, teamName, authToken } = data;
233
245
  const modelId = MODEL_MAP[model] || MODEL_MAP.opus; // Inherit parent default
234
246
  const startTime = Date.now();
247
+ // Load behavioral config from DB — no hardcoded values
248
+ await resolveTeammateConfig();
235
249
  // Initialize telemetry client with auth token if provided
236
250
  if (authToken) {
237
251
  initializeTelemetryClient(authToken);
@@ -343,15 +357,15 @@ async function runTeammateLoop(data) {
343
357
  let outerTurn = 0;
344
358
  while (true) {
345
359
  // Safety: max turns guard
346
- if (++outerTurn > MAX_TEAMMATE_TURNS) {
360
+ if (++outerTurn > TEAMMATE_MAX_TOTAL_TURNS) {
347
361
  logTeammateComplete("max_turns_reached");
348
- report({ type: "done", teammateId, content: `${teammateName} reached max turns (${MAX_TEAMMATE_TURNS})`, tokensUsed: { input: totalIn, output: totalOut } });
362
+ report({ type: "done", teammateId, content: `${teammateName} reached max turns (${TEAMMATE_MAX_TOTAL_TURNS})`, tokensUsed: { input: totalIn, output: totalOut } });
349
363
  break;
350
364
  }
351
365
  // Safety: elapsed time guard
352
- if (Date.now() - startTime > MAX_TEAMMATE_DURATION_MS) {
366
+ if (Date.now() - startTime > TEAMMATE_MAX_DURATION_MS) {
353
367
  logTeammateComplete("timeout");
354
- report({ type: "done", teammateId, content: `${teammateName} timed out after ${Math.round(MAX_TEAMMATE_DURATION_MS / 60_000)}min`, tokensUsed: { input: totalIn, output: totalOut } });
368
+ report({ type: "done", teammateId, content: `${teammateName} timed out after ${Math.round(TEAMMATE_MAX_DURATION_MS / 60_000)}min`, tokensUsed: { input: totalIn, output: totalOut } });
355
369
  break;
356
370
  }
357
371
  const team = loadTeam(teamId);
@@ -389,7 +403,7 @@ async function runTeammateLoop(data) {
389
403
  // Resolve per-task model: task-level → team default
390
404
  const taskModel = claimed.model || model;
391
405
  currentTaskModelId = MODEL_MAP[taskModel] || MODEL_MAP[model] || MODEL_MAP.opus;
392
- // Disable thinking for teammates — with MAX_OUTPUT_TOKENS=16K, adaptive thinking
406
+ // Disable thinking for teammates — with TEAMMATE_MAX_OUTPUT_TOKENS=16K, adaptive thinking
393
407
  // burns 8-12K tokens on reasoning per turn, leaving only 4-8K for actual code output.
394
408
  // This caused catastrophic token bloat (359K input tokens per task) and truncated responses.
395
409
  currentTaskThinking = false;
@@ -407,7 +421,7 @@ async function runTeammateLoop(data) {
407
421
  const systemPrompt = buildTeammatePrompt(teammateName, freshTeam, freshTask, cwd);
408
422
  // Agent loop for current task
409
423
  let apiError = null;
410
- for (let turn = 0; turn < MAX_TURNS_PER_TASK; turn++) {
424
+ for (let turn = 0; turn < TEAMMATE_MAX_TURNS_PER_TASK; turn++) {
411
425
  const apiStart = Date.now();
412
426
  let response;
413
427
  try {
@@ -0,0 +1,6 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export declare function handleApiDocs(_sb: SupabaseClient, args: Record<string, unknown>, _storeId?: string): Promise<{
3
+ success: boolean;
4
+ data?: unknown;
5
+ error?: string;
6
+ }>;