titan-agent 5.4.0 → 5.4.2

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 (48) hide show
  1. package/dist/agent/agent.js +1 -1
  2. package/dist/agent/agent.js.map +1 -1
  3. package/dist/agent/agentLoop.js +77 -12
  4. package/dist/agent/agentLoop.js.map +1 -1
  5. package/dist/agent/agentWakeup.js +8 -3
  6. package/dist/agent/agentWakeup.js.map +1 -1
  7. package/dist/agent/commandPost.js +6 -1
  8. package/dist/agent/commandPost.js.map +1 -1
  9. package/dist/agent/heartbeatScheduler.js +36 -4
  10. package/dist/agent/heartbeatScheduler.js.map +1 -1
  11. package/dist/agent/toolRunner.js +30 -0
  12. package/dist/agent/toolRunner.js.map +1 -1
  13. package/dist/config/config.js +30 -8
  14. package/dist/config/config.js.map +1 -1
  15. package/dist/config/schema.js +10 -1
  16. package/dist/config/schema.js.map +1 -1
  17. package/dist/eval/record.js +1 -1
  18. package/dist/eval/record.js.map +1 -1
  19. package/dist/gateway/server.js +26 -0
  20. package/dist/gateway/server.js.map +1 -1
  21. package/dist/mesh/transport.js +60 -8
  22. package/dist/mesh/transport.js.map +1 -1
  23. package/dist/providers/anthropic.js +3 -2
  24. package/dist/providers/anthropic.js.map +1 -1
  25. package/dist/providers/base.js.map +1 -1
  26. package/dist/providers/google.js +94 -20
  27. package/dist/providers/google.js.map +1 -1
  28. package/dist/providers/modelCapabilities.js +59 -0
  29. package/dist/providers/modelCapabilities.js.map +1 -0
  30. package/dist/providers/ollama.js +3 -2
  31. package/dist/providers/ollama.js.map +1 -1
  32. package/dist/providers/openai.js +4 -3
  33. package/dist/providers/openai.js.map +1 -1
  34. package/dist/providers/openai_compat.js +3 -2
  35. package/dist/providers/openai_compat.js.map +1 -1
  36. package/dist/providers/router.js +63 -21
  37. package/dist/providers/router.js.map +1 -1
  38. package/dist/skills/registry.js +176 -163
  39. package/dist/skills/registry.js.map +1 -1
  40. package/dist/telemetry/activityLog.js +1 -1
  41. package/dist/telemetry/activityLog.js.map +1 -1
  42. package/dist/utils/constants.js +2 -2
  43. package/dist/utils/constants.js.map +1 -1
  44. package/docs/AGENT-HIERARCHY.md +154 -0
  45. package/docs/superpowers/plans/2026-04-29-titan-production-fix.md +241 -0
  46. package/package.json +2 -2
  47. package/scripts/start-workers.sh +39 -0
  48. package/scripts/task-feeder.ts +38 -0
@@ -47,8 +47,32 @@ function findNextHop(destinationNodeId) {
47
47
  routingTable.delete(destinationNodeId);
48
48
  return null;
49
49
  }
50
+ const nextHopWs = peerConnections.get(entry.nextHopNodeId);
51
+ if (!nextHopWs || nextHopWs.readyState !== WebSocket.OPEN) {
52
+ routingTable.delete(destinationNodeId);
53
+ logger.debug(COMPONENT, `Pruned route to ${destinationNodeId} via dead next-hop ${entry.nextHopNodeId}`);
54
+ return null;
55
+ }
50
56
  return entry.nextHopNodeId;
51
57
  }
58
+ function invalidateRoutesVia(disconnectedNodeId) {
59
+ let removed = 0;
60
+ for (const [dest, entry] of routingTable) {
61
+ if (entry.nextHopNodeId === disconnectedNodeId || dest === disconnectedNodeId) {
62
+ routingTable.delete(dest);
63
+ removed++;
64
+ }
65
+ }
66
+ if (removed > 0) {
67
+ logger.info(COMPONENT, `Mesh peer ${disconnectedNodeId} dropped \u2014 invalidated ${removed} route(s); broadcasting refresh`);
68
+ try {
69
+ broadcastRouteAdvertisement();
70
+ } catch (err) {
71
+ logger.debug(COMPONENT, `Route refresh broadcast failed: ${err.message}`);
72
+ }
73
+ }
74
+ return removed;
75
+ }
52
76
  function upsertRoute(entry) {
53
77
  const existing = routingTable.get(entry.destinationNodeId);
54
78
  if (!existing || entry.cost < existing.cost) {
@@ -239,6 +263,7 @@ async function connectToPeer(address, port, localNodeId, meshSecret) {
239
263
  pendingRequests.delete(reqId);
240
264
  }
241
265
  }
266
+ invalidateRoutesVia(remoteNodeId);
242
267
  logger.info(COMPONENT, `Peer disconnected: ${remoteNodeId}`);
243
268
  }
244
269
  if (!resolved) {
@@ -393,6 +418,8 @@ function handleMeshWebSocket(ws, nodeId, localNodeId, onTaskRequest) {
393
418
  if (innerAction === "task_request" && onTaskRequest && msg.requestId) {
394
419
  activeRemoteTasks++;
395
420
  let replied = false;
421
+ const originalRequesterId = msg.fromNodeId;
422
+ const originalRequestId = msg.requestId;
396
423
  const sendReply = (payload) => {
397
424
  if (replied) return;
398
425
  replied = true;
@@ -401,18 +428,40 @@ function handleMeshWebSocket(ws, nodeId, localNodeId, onTaskRequest) {
401
428
  type: "mesh",
402
429
  action: "task_response",
403
430
  fromNodeId: localNodeId2,
404
- toNodeId: msg.fromNodeId,
405
- requestId: msg.requestId,
406
- payload,
431
+ toNodeId: originalRequesterId,
432
+ requestId: originalRequestId,
433
+ payload: { ...payload, originalRequesterId },
407
434
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
408
435
  };
409
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(reply));
436
+ if (peerConnections.has(originalRequesterId)) {
437
+ sendToPeer(originalRequesterId, reply);
438
+ return;
439
+ }
440
+ const routed = routeMessageMultiHop(originalRequesterId, {
441
+ ...reply,
442
+ action: "route_forward",
443
+ payload: {
444
+ innerAction: "task_response",
445
+ originalRequesterId,
446
+ ...payload
447
+ }
448
+ });
449
+ if (!routed && ws.readyState === WebSocket.OPEN) {
450
+ ws.send(JSON.stringify(reply));
451
+ }
410
452
  };
411
453
  try {
412
454
  onTaskRequest({ ...msg, action: "task_request" }, sendReply);
413
455
  } catch (err) {
414
456
  sendReply({ error: `Handler error: ${err.message}` });
415
457
  }
458
+ } else if (innerAction === "task_response" && msg.requestId) {
459
+ const pending = pendingRequests.get(msg.requestId);
460
+ if (pending) {
461
+ clearTimeout(pending.timeout);
462
+ pendingRequests.delete(msg.requestId);
463
+ pending.resolve(msg.payload);
464
+ }
416
465
  }
417
466
  } else {
418
467
  routeMessageMultiHop(msg.toNodeId, msg);
@@ -421,17 +470,20 @@ function handleMeshWebSocket(ws, nodeId, localNodeId, onTaskRequest) {
421
470
  } catch {
422
471
  }
423
472
  });
424
- ws.on("close", () => {
473
+ const cleanup = (cause) => {
425
474
  peerConnections.delete(nodeId);
426
475
  for (const [reqId, req] of pendingRequests) {
427
476
  if (req.peerNodeId === nodeId) {
428
477
  clearTimeout(req.timeout);
429
- req.reject(new Error(`Peer disconnected: ${nodeId}`));
478
+ req.reject(new Error(`Peer ${cause}: ${nodeId}`));
430
479
  pendingRequests.delete(reqId);
431
480
  }
432
481
  }
433
- logger.info(COMPONENT, `Mesh peer disconnected: ${nodeId}`);
434
- });
482
+ invalidateRoutesVia(nodeId);
483
+ logger.info(COMPONENT, `Mesh peer disconnected (${cause}): ${nodeId}`);
484
+ };
485
+ ws.on("close", () => cleanup("close"));
486
+ ws.on("error", () => cleanup("error"));
435
487
  }
436
488
  function startHeartbeat(localNodeId, payload, intervalMs = 6e4) {
437
489
  if (heartbeatInterval) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/mesh/transport.ts"],"sourcesContent":["/**\n * TITAN — Mesh Transport Layer\n * WebSocket peer-to-peer connections between TITAN nodes.\n * Reuses the existing gateway WS server — no new protocol needed.\n */\nimport { WebSocket } from 'ws';\nimport { createHmac, timingSafeEqual } from 'crypto';\nimport logger from '../utils/logger.js';\nimport { registerPeer } from './discovery.js';\nimport { getOrCreateNodeId } from './identity.js';\n\nconst COMPONENT = 'MeshTransport';\n\n// ── Active WebSocket connections to peers ──────────────────────\nconst peerConnections = new Map<string, WebSocket>();\nconst pendingRequests = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timeout: ReturnType<typeof setTimeout>; peerNodeId?: string }>();\nconst reconnectState = new Map<string, { attempts: number; nodeId: string; address: string; port: number; meshSecret: string; timer?: ReturnType<typeof setTimeout> }>();\nconst peerUseTls = new Map<string, boolean>();\n\n/** Reconnection config constants.\n *\n * Hunt Finding #45 (2026-04-15): README promises \"reconnect automatically on\n * restart.\" The mesh DID reconnect, but the exponential backoff was tuned\n * too loose for a LAN mesh — attempts 5-6 slept 35s and 54s respectively,\n * meaning a Mini PC restart could leave the mesh degraded for 2+ minutes.\n * Tightened the cap from 60s → 30s so worst-case gap after restart is ~35s.\n */\nconst RECONNECT_BASE_DELAY = 2000;\nconst RECONNECT_MAX_DELAY = 30000;\nconst RECONNECT_JITTER_FRAC = 0.2; // ±20% jitter\nconst DEFAULT_MAX_RECONNECT_ATTEMPTS = 0; // 0 = unlimited reconnection attempts\n\n/** Mark a peer as using TLS (called by discovery after successful HTTPS probe) */\nexport function setPeerTls(address: string, port: number, tls: boolean): void {\n peerUseTls.set(`${address}:${port}`, tls);\n}\n\n/**\n * Update reconnect state when a peer moves to a new address (mDNS/Tailscale rediscovery).\n * Preserves the current attempt counter but points reconnect at the new endpoint.\n */\nexport function updatePeerAddress(\n oldAddress: string,\n oldPort: number,\n newAddress: string,\n newPort: number,\n): void {\n const oldKey = `${oldAddress}:${oldPort}`;\n const newKey = `${newAddress}:${newPort}`;\n const state = reconnectState.get(oldKey);\n if (state && (oldAddress !== newAddress || oldPort !== newPort)) {\n if (state.timer) clearTimeout(state.timer);\n reconnectState.set(newKey, { ...state, address: newAddress, port: newPort });\n reconnectState.delete(oldKey);\n logger.debug(COMPONENT, `Peer address updated: ${oldKey} → ${newKey}`);\n }\n}\n\nlet heartbeatInterval: ReturnType<typeof setInterval> | null = null;\nlet activeRemoteTasks = 0;\n\n/** Get current number of active remote tasks being processed */\nexport function getActiveRemoteTaskCount(): number {\n return activeRemoteTasks;\n}\n\n/** Mesh message format */\nexport interface MeshMessage {\n type: 'mesh';\n action: 'heartbeat' | 'task_request' | 'task_response' | 'model_query' | 'model_list' | 'route_broadcast' | 'route_forward';\n fromNodeId: string;\n toNodeId?: string;\n requestId?: string;\n payload: Record<string, unknown>;\n timestamp: string;\n /** TTL for multi-hop routing (decremented at each hop) */\n ttl?: number;\n /** Hop counter for loop detection */\n hopCount?: number;\n /** Visited node IDs for loop detection */\n visitedNodes?: string[];\n}\n\n/** Routing table entry: next-hop info to reach a destination node */\ninterface RouteEntry {\n destinationNodeId: string;\n nextHopNodeId: string; // immediate next hop\n cost: number; // distance metric (hops, RTT, etc.)\n discoveredAt: number;\n lastUsedAt: number;\n}\n\n/** Broadcasted route advertisement from a peer */\ninterface RouteAdvertisement {\n destinationNodeId: string;\n cost: number;\n visitedNodes: string[]; // path taken so far\n}\n\n// ── Routing table ──────────────────────────────────────────────────\nconst routingTable = new Map<string, RouteEntry>();\nconst MAX_ROUTE_TTL = 10;\nconst ROUTE_PRUNE_INTERVAL_MS = 60_000;\nconst ROUTE_STALE_MS = 300_000; // 5 minutes\nlet routeBroadcastInterval: ReturnType<typeof setInterval> | null = null;\n\n/** Get all routing table entries */\nexport function getRoutingTable(): RouteEntry[] {\n return Array.from(routingTable.values());\n}\n\n/** Find the next-hop node to reach a destination */\nexport function findNextHop(destinationNodeId: string): string | null {\n const entry = routingTable.get(destinationNodeId);\n if (!entry) return null;\n // Check if route is stale\n if (Date.now() - entry.lastUsedAt > ROUTE_STALE_MS) {\n routingTable.delete(destinationNodeId);\n return null;\n }\n return entry.nextHopNodeId;\n}\n\n/** Update or insert a routing table entry */\nfunction upsertRoute(entry: RouteEntry): void {\n const existing = routingTable.get(entry.destinationNodeId);\n if (!existing || entry.cost < existing.cost) {\n routingTable.set(entry.destinationNodeId, entry);\n } else if (entry.destinationNodeId === entry.nextHopNodeId) {\n // Always prefer direct routes even if cost is equal\n routingTable.set(entry.destinationNodeId, entry);\n }\n}\n\n/** Prune stale routes */\nfunction pruneStaleRoutes(): void {\n const cutoff = Date.now() - ROUTE_STALE_MS;\n for (const [dest, entry] of routingTable) {\n if (entry.lastUsedAt < cutoff) {\n routingTable.delete(dest);\n logger.debug(COMPONENT, `Pruned stale route to ${dest}`);\n }\n }\n}\n\n/** Handle incoming route broadcast advertisements */\nfunction handleRouteBroadcast(msg: MeshMessage, fromNodeId: string): void {\n if (msg.action !== 'route_broadcast' || !msg.payload.advertisements) return;\n\n const ads = msg.payload.advertisements as RouteAdvertisement[];\n const localNodeId = getOrCreateNodeId();\n let updated = false;\n\n for (const ad of ads) {\n // Skip if this node is the destination\n if (ad.destinationNodeId === localNodeId) continue;\n\n // Skip if we've already seen a better path\n const newCost = ad.cost + 1;\n const existing = routingTable.get(ad.destinationNodeId);\n if (existing && existing.cost <= newCost) continue;\n\n // Skip if path would create a loop (our nodeId in visited list)\n if (ad.visitedNodes.includes(localNodeId)) continue;\n\n upsertRoute({\n destinationNodeId: ad.destinationNodeId,\n nextHopNodeId: fromNodeId,\n cost: newCost,\n discoveredAt: Date.now(),\n lastUsedAt: Date.now(),\n });\n updated = true;\n }\n\n if (updated) {\n logger.debug(COMPONENT, `Updated routing table from broadcast by ${fromNodeId}, table size: ${routingTable.size}`);\n }\n}\n\n/** Broadcast our routing table to all connected peers (distance-vector) */\nfunction broadcastRouteAdvertisement(): void {\n const localNodeId = getOrCreateNodeId();\n const advertisements: RouteAdvertisement[] = [];\n\n // Advertise ourselves\n advertisements.push({\n destinationNodeId: localNodeId,\n cost: 0,\n visitedNodes: [localNodeId],\n });\n\n // Advertise routes we know about\n for (const [, entry] of routingTable) {\n advertisements.push({\n destinationNodeId: entry.destinationNodeId,\n cost: entry.cost,\n visitedNodes: [localNodeId],\n });\n }\n\n const msg: MeshMessage = {\n type: 'mesh',\n action: 'route_broadcast',\n fromNodeId: localNodeId,\n payload: { advertisements },\n timestamp: new Date().toISOString(),\n };\n broadcastToMesh(msg);\n}\n\n/** Start route broadcast interval */\nexport function startRouteBroadcast(intervalMs = 30_000): void {\n if (routeBroadcastInterval) return;\n broadcastRouteAdvertisement(); // Immediate initial broadcast\n routeBroadcastInterval = setInterval(() => {\n // Prune stale routes first\n pruneStaleRoutes();\n broadcastRouteAdvertisement();\n }, intervalMs);\n logger.debug(COMPONENT, `Route broadcast started (${Math.round(intervalMs / 1000)}s)`);\n}\n\nexport function stopRouteBroadcast(): void {\n if (routeBroadcastInterval) {\n clearInterval(routeBroadcastInterval);\n routeBroadcastInterval = null;\n }\n routingTable.clear();\n}\n\n/** Route a message through the mesh using the routing table (multi-hop) */\nexport function routeMessageMultiHop(\n destinationNodeId: string,\n message: MeshMessage,\n): boolean {\n const localNodeId = getOrCreateNodeId();\n\n // Already at destination\n if (destinationNodeId === localNodeId) return false;\n\n // Direct connection — send straight there\n if (peerConnections.has(destinationNodeId)) {\n return sendToPeer(destinationNodeId, message);\n }\n\n // Multi-hop: find next hop\n const nextHop = findNextHop(destinationNodeId);\n if (!nextHop) {\n logger.warn(COMPONENT, `No route to ${destinationNodeId}`);\n return false;\n }\n\n // Check TTL\n message.ttl = (message.ttl ?? MAX_ROUTE_TTL) - 1;\n if (message.ttl <= 0) {\n logger.warn(COMPONENT, `TTL expired for message to ${destinationNodeId}`);\n return false;\n }\n\n // Loop detection\n message.visitedNodes = [...(message.visitedNodes || []), localNodeId];\n if (message.visitedNodes.includes(nextHop)) {\n logger.warn(COMPONENT, `Loop detected: ${nextHop} already visited for dest ${destinationNodeId}`);\n return false;\n }\n\n message.hopCount = (message.hopCount || 0) + 1;\n\n // Forward to next hop\n const sent = sendToPeer(nextHop, message);\n if (sent) {\n // Update lastUsed\n const entry = routingTable.get(destinationNodeId);\n if (entry) entry.lastUsedAt = Date.now();\n logger.debug(COMPONENT, `Forwarded to ${nextHop} for ${destinationNodeId} (hop ${message.hopCount})`);\n }\n return sent;\n}\n\n/** Generate HMAC auth token for mesh handshake */\nexport function generateMeshAuth(nodeId: string, meshSecret: string): string {\n const ts = Math.floor(Date.now() / 30000).toString(); // 30-second window\n return createHmac('sha256', meshSecret).update(ts + nodeId).digest('hex');\n}\n\n/** Verify HMAC auth token */\nexport function verifyMeshAuth(token: string, nodeId: string, meshSecret: string): boolean {\n const now = Math.floor(Date.now() / 30000);\n // Check current and previous window to handle clock skew\n for (const ts of [now.toString(), (now - 1).toString()]) {\n const expected = createHmac('sha256', meshSecret).update(ts + nodeId).digest('hex');\n if (token.length === expected.length && timingSafeEqual(Buffer.from(token), Buffer.from(expected))) return true;\n }\n return false;\n}\n\n/** Connect to a peer via WebSocket */\nexport async function connectToPeer(\n address: string,\n port: number,\n localNodeId: string,\n meshSecret: string,\n): Promise<boolean> {\n const peerKey = `${address}:${port}`;\n\n return new Promise((resolve) => {\n const auth = generateMeshAuth(localNodeId, meshSecret);\n // Try wss:// first (TITAN auto-HTTPS via mkcert), fall back to ws://\n const wsUrl = `ws://${address}:${port}?mesh=true&nodeId=${localNodeId}&auth=${auth}`;\n const wssUrl = `wss://${address}:${port}?mesh=true&nodeId=${localNodeId}&auth=${auth}`;\n const url = peerUseTls.get(peerKey) ? wssUrl : wsUrl;\n\n const ws = new WebSocket(url, { handshakeTimeout: 5000 });\n let remoteNodeId: string | null = null;\n let resolved = false;\n\n ws.on('open', () => {\n logger.info(COMPONENT, `Connected to peer at ${address}:${port}`);\n // Reset reconnect state on successful connection\n const existing = reconnectState.get(peerKey);\n if (existing?.timer) clearTimeout(existing.timer);\n reconnectState.delete(peerKey);\n });\n\n ws.on('message', (data) => {\n try {\n const msg = JSON.parse(data.toString()) as MeshMessage;\n if (msg.type !== 'mesh') return;\n\n if (msg.action === 'heartbeat' && msg.fromNodeId) {\n remoteNodeId = msg.fromNodeId;\n peerConnections.set(remoteNodeId, ws);\n registerPeer({\n nodeId: remoteNodeId,\n hostname: (msg.payload?.hostname as string) || address,\n address,\n port,\n version: (msg.payload?.version as string) || 'unknown',\n models: (msg.payload?.models as string[]) || [],\n agentCount: (msg.payload?.agentCount as number) || 0,\n load: (msg.payload?.load as number) || 0,\n discoveredVia: 'manual',\n lastSeen: Date.now(),\n });\n if (!resolved) { resolved = true; resolve(true); }\n }\n\n if (msg.action === 'task_response' && msg.requestId) {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(msg.requestId);\n pending.resolve(msg.payload);\n }\n }\n } catch {\n // Ignore malformed messages\n }\n });\n\n ws.on('error', () => {\n if (!resolved) { resolved = true; resolve(false); }\n });\n\n ws.on('close', () => {\n if (remoteNodeId) {\n peerConnections.delete(remoteNodeId);\n // Reject any pending requests targeted at this peer\n for (const [reqId, req] of pendingRequests) {\n if (req.peerNodeId === remoteNodeId) {\n clearTimeout(req.timeout);\n req.reject(new Error(`Peer disconnected: ${remoteNodeId}`));\n pendingRequests.delete(reqId);\n }\n }\n logger.info(COMPONENT, `Peer disconnected: ${remoteNodeId}`);\n }\n if (!resolved) { resolved = true; resolve(false); }\n\n // ── Reconnect with exponential backoff + jitter ──\n const existing = reconnectState.get(peerKey);\n if (existing && existing.address === address && existing.port === port) {\n // Existing reconnect state with matching address — bump attempt counter\n existing.attempts++;\n } else if (existing) {\n // Address changed (e.g., new mDNS/Tailscale discovery) — reset\n if (existing.timer) clearTimeout(existing.timer);\n reconnectState.set(peerKey, { attempts: 1, nodeId: remoteNodeId || existing.nodeId, address, port, meshSecret });\n } else {\n reconnectState.set(peerKey, { attempts: 1, nodeId: remoteNodeId || 'unknown', address, port, meshSecret });\n }\n\n const state = reconnectState.get(peerKey)!;\n const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(2, state.attempts - 1), RECONNECT_MAX_DELAY);\n const jitter = delay * RECONNECT_JITTER_FRAC * (Math.random() * 2 - 1);\n const jitteredDelay = Math.max(100, Math.round(delay + jitter));\n\n logger.info(COMPONENT, `Reconnecting to ${address}:${port} in ${jitteredDelay}ms (attempt ${state.attempts})`);\n\n const timer = setTimeout(() => {\n connectToPeer(address, port, localNodeId, meshSecret).catch(e => logger.debug(COMPONENT, `Background reconnect failed: ${(e as Error).message}`));\n }, jitteredDelay);\n state.timer = timer;\n });\n\n // Timeout\n setTimeout(() => {\n if (ws.readyState !== WebSocket.OPEN) {\n ws.close();\n if (!resolved) { resolved = true; resolve(false); }\n }\n }, 5000);\n });\n}\n\n/** Send a message to a specific peer */\nexport function sendToPeer(nodeId: string, message: MeshMessage): boolean {\n const ws = peerConnections.get(nodeId);\n if (!ws || ws.readyState !== WebSocket.OPEN) return false;\n ws.send(JSON.stringify(message));\n return true;\n}\n\n/** Broadcast a message to all connected peers */\nexport function broadcastToMesh(message: MeshMessage): void {\n const data = JSON.stringify(message);\n for (const [nodeId, ws] of peerConnections) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(data);\n } else {\n peerConnections.delete(nodeId);\n }\n }\n}\n\n/** Route a task to a remote node and await the response (supports multi-hop) */\nexport function routeTaskToNode(\n nodeId: string,\n requestId: string,\n message: string,\n model: string,\n timeoutMs = 60_000,\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingRequests.delete(requestId);\n reject(new Error(`Mesh task timed out (peer: ${nodeId})`));\n }, timeoutMs);\n\n pendingRequests.set(requestId, { resolve, reject, timeout, peerNodeId: nodeId });\n\n // First try direct send\n let sent = sendToPeer(nodeId, {\n type: 'mesh',\n action: 'task_request',\n fromNodeId: getOrCreateNodeId(),\n toNodeId: nodeId,\n requestId,\n payload: { message, model },\n timestamp: new Date().toISOString(),\n });\n\n // If no direct connection, try multi-hop routing\n if (!sent) {\n sent = routeMessageMultiHop(nodeId, {\n type: 'mesh',\n action: 'route_forward',\n fromNodeId: getOrCreateNodeId(),\n toNodeId: nodeId,\n requestId,\n payload: {\n innerAction: 'task_request',\n message,\n model,\n },\n timestamp: new Date().toISOString(),\n });\n }\n\n if (!sent) {\n clearTimeout(timeout);\n pendingRequests.delete(requestId);\n reject(new Error(`Cannot reach peer: ${nodeId} (no direct or multi-hop route)`));\n }\n });\n}\n\n/** Handle an incoming mesh WebSocket connection (called from gateway) */\nexport function handleMeshWebSocket(\n ws: WebSocket,\n nodeId: string,\n localNodeId: string,\n onTaskRequest?: (msg: MeshMessage, reply: (payload: Record<string, unknown>) => void) => void,\n): void {\n peerConnections.set(nodeId, ws);\n logger.info(COMPONENT, `Mesh peer connected: ${nodeId}`);\n\n ws.on('message', (data) => {\n try {\n const msg = JSON.parse(data.toString()) as MeshMessage;\n if (msg.type !== 'mesh') return;\n\n if (msg.action === 'heartbeat') {\n registerPeer({\n nodeId: msg.fromNodeId,\n hostname: (msg.payload?.hostname as string) || 'unknown',\n address: '', // Already connected\n port: 0,\n version: (msg.payload?.version as string) || 'unknown',\n models: (msg.payload?.models as string[]) || [],\n agentCount: (msg.payload?.agentCount as number) || 0,\n load: (msg.payload?.load as number) || 0,\n discoveredVia: 'manual',\n lastSeen: Date.now(),\n });\n }\n\n if (msg.action === 'task_request' && onTaskRequest && msg.requestId) {\n activeRemoteTasks++;\n let replied = false;\n const sendReply = (payload: Record<string, unknown>) => {\n if (replied) return;\n replied = true;\n activeRemoteTasks = Math.max(0, activeRemoteTasks - 1);\n const reply: MeshMessage = {\n type: 'mesh',\n action: 'task_response',\n fromNodeId: localNodeId,\n toNodeId: msg.fromNodeId,\n requestId: msg.requestId,\n payload,\n timestamp: new Date().toISOString(),\n };\n if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(reply));\n };\n try {\n onTaskRequest(msg, sendReply);\n } catch (err) {\n sendReply({ error: `Handler error: ${(err as Error).message}` });\n }\n }\n\n if (msg.action === 'task_response' && msg.requestId) {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(msg.requestId);\n pending.resolve(msg.payload);\n }\n }\n\n // Handle route broadcast advertisements (multi-hop routing)\n if (msg.action === 'route_broadcast') {\n handleRouteBroadcast(msg, nodeId);\n }\n\n // Handle forwarded messages (multi-hop routing)\n if (msg.action === 'route_forward' && msg.toNodeId) {\n const localNodeId = getOrCreateNodeId();\n // Forwarded message arrived — if we are the destination, process it, otherwise re-forward\n if (msg.toNodeId === localNodeId) {\n // This is for us — treat as the inner message\n const innerAction = (msg.payload.innerAction as MeshMessage['action']) || 'task_request';\n if (innerAction === 'task_request' && onTaskRequest && msg.requestId) {\n activeRemoteTasks++;\n let replied = false;\n const sendReply = (payload: Record<string, unknown>) => {\n if (replied) return;\n replied = true;\n activeRemoteTasks = Math.max(0, activeRemoteTasks - 1);\n const reply: MeshMessage = {\n type: 'mesh',\n action: 'task_response',\n fromNodeId: localNodeId,\n toNodeId: msg.fromNodeId,\n requestId: msg.requestId,\n payload,\n timestamp: new Date().toISOString(),\n };\n if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(reply));\n };\n try {\n onTaskRequest({ ...msg, action: 'task_request' }, sendReply);\n } catch (err) {\n sendReply({ error: `Handler error: ${(err as Error).message}` });\n }\n }\n } else {\n // Not for us — forward along the route\n routeMessageMultiHop(msg.toNodeId, msg);\n }\n }\n } catch {\n // Ignore\n }\n });\n\n ws.on('close', () => {\n peerConnections.delete(nodeId);\n // Reject any pending requests for this peer\n for (const [reqId, req] of pendingRequests) {\n if (req.peerNodeId === nodeId) {\n clearTimeout(req.timeout);\n req.reject(new Error(`Peer disconnected: ${nodeId}`));\n pendingRequests.delete(reqId);\n }\n }\n logger.info(COMPONENT, `Mesh peer disconnected: ${nodeId}`);\n });\n}\n\n/** Start sending periodic heartbeats to all connected peers */\nexport function startHeartbeat(\n localNodeId: string,\n payload?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>),\n intervalMs = 60_000,\n): void {\n if (heartbeatInterval) return; // Already running\n heartbeatInterval = setInterval(async () => {\n const resolved = typeof payload === 'function' ? await payload() : (payload || {});\n const msg: MeshMessage = {\n type: 'mesh',\n action: 'heartbeat',\n fromNodeId: localNodeId,\n payload: resolved,\n timestamp: new Date().toISOString(),\n };\n broadcastToMesh(msg);\n }, intervalMs);\n logger.debug(COMPONENT, `Heartbeat interval started (${Math.round(intervalMs / 1000)}s)`);\n}\n\n/** Stop the heartbeat interval */\nexport function stopHeartbeat(): void {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval);\n heartbeatInterval = null;\n logger.debug(COMPONENT, 'Heartbeat interval stopped');\n }\n}\n\n/** Get connected peer count */\nexport function getConnectedPeerCount(): number {\n let count = 0;\n for (const [, ws] of peerConnections) {\n if (ws.readyState === WebSocket.OPEN) count++;\n }\n return count;\n}\n\n/** Disconnect a specific peer */\nexport function disconnectPeer(nodeId: string): void {\n const ws = peerConnections.get(nodeId);\n if (ws) {\n ws.close();\n peerConnections.delete(nodeId);\n // Reject any pending requests\n for (const [reqId, req] of pendingRequests) {\n if (req.peerNodeId === nodeId) {\n clearTimeout(req.timeout);\n req.reject(new Error(`Peer disconnected: ${nodeId}`));\n pendingRequests.delete(reqId);\n }\n }\n logger.info(COMPONENT, `Peer disconnected (requested): ${nodeId}`);\n }\n}\n\n/** Disconnect all peers */\nexport function disconnectAll(): void {\n stopHeartbeat();\n stopRouteBroadcast();\n for (const [nodeId, ws] of peerConnections) {\n ws.close();\n peerConnections.delete(nodeId);\n }\n for (const [id, req] of pendingRequests) {\n clearTimeout(req.timeout);\n req.reject(new Error('Mesh shutting down'));\n pendingRequests.delete(id);\n }\n reconnectState.clear();\n}\n"],"mappings":"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","names":["localNodeId"]}
1
+ {"version":3,"sources":["../../src/mesh/transport.ts"],"sourcesContent":["/**\n * TITAN — Mesh Transport Layer\n * WebSocket peer-to-peer connections between TITAN nodes.\n * Reuses the existing gateway WS server — no new protocol needed.\n */\nimport { WebSocket } from 'ws';\nimport { createHmac, timingSafeEqual } from 'crypto';\nimport logger from '../utils/logger.js';\nimport { registerPeer } from './discovery.js';\nimport { getOrCreateNodeId } from './identity.js';\n\nconst COMPONENT = 'MeshTransport';\n\n// ── Active WebSocket connections to peers ──────────────────────\nconst peerConnections = new Map<string, WebSocket>();\nconst pendingRequests = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timeout: ReturnType<typeof setTimeout>; peerNodeId?: string }>();\nconst reconnectState = new Map<string, { attempts: number; nodeId: string; address: string; port: number; meshSecret: string; timer?: ReturnType<typeof setTimeout> }>();\nconst peerUseTls = new Map<string, boolean>();\n\n/** Reconnection config constants.\n *\n * Hunt Finding #45 (2026-04-15): README promises \"reconnect automatically on\n * restart.\" The mesh DID reconnect, but the exponential backoff was tuned\n * too loose for a LAN mesh — attempts 5-6 slept 35s and 54s respectively,\n * meaning a Mini PC restart could leave the mesh degraded for 2+ minutes.\n * Tightened the cap from 60s → 30s so worst-case gap after restart is ~35s.\n */\nconst RECONNECT_BASE_DELAY = 2000;\nconst RECONNECT_MAX_DELAY = 30000;\nconst RECONNECT_JITTER_FRAC = 0.2; // ±20% jitter\nconst DEFAULT_MAX_RECONNECT_ATTEMPTS = 0; // 0 = unlimited reconnection attempts\n\n/** Mark a peer as using TLS (called by discovery after successful HTTPS probe) */\nexport function setPeerTls(address: string, port: number, tls: boolean): void {\n peerUseTls.set(`${address}:${port}`, tls);\n}\n\n/**\n * Update reconnect state when a peer moves to a new address (mDNS/Tailscale rediscovery).\n * Preserves the current attempt counter but points reconnect at the new endpoint.\n */\nexport function updatePeerAddress(\n oldAddress: string,\n oldPort: number,\n newAddress: string,\n newPort: number,\n): void {\n const oldKey = `${oldAddress}:${oldPort}`;\n const newKey = `${newAddress}:${newPort}`;\n const state = reconnectState.get(oldKey);\n if (state && (oldAddress !== newAddress || oldPort !== newPort)) {\n if (state.timer) clearTimeout(state.timer);\n reconnectState.set(newKey, { ...state, address: newAddress, port: newPort });\n reconnectState.delete(oldKey);\n logger.debug(COMPONENT, `Peer address updated: ${oldKey} → ${newKey}`);\n }\n}\n\nlet heartbeatInterval: ReturnType<typeof setInterval> | null = null;\nlet activeRemoteTasks = 0;\n\n/** Get current number of active remote tasks being processed */\nexport function getActiveRemoteTaskCount(): number {\n return activeRemoteTasks;\n}\n\n/** Mesh message format */\nexport interface MeshMessage {\n type: 'mesh';\n action: 'heartbeat' | 'task_request' | 'task_response' | 'model_query' | 'model_list' | 'route_broadcast' | 'route_forward';\n fromNodeId: string;\n toNodeId?: string;\n requestId?: string;\n payload: Record<string, unknown>;\n timestamp: string;\n /** TTL for multi-hop routing (decremented at each hop) */\n ttl?: number;\n /** Hop counter for loop detection */\n hopCount?: number;\n /** Visited node IDs for loop detection */\n visitedNodes?: string[];\n}\n\n/** Routing table entry: next-hop info to reach a destination node */\ninterface RouteEntry {\n destinationNodeId: string;\n nextHopNodeId: string; // immediate next hop\n cost: number; // distance metric (hops, RTT, etc.)\n discoveredAt: number;\n lastUsedAt: number;\n}\n\n/** Broadcasted route advertisement from a peer */\ninterface RouteAdvertisement {\n destinationNodeId: string;\n cost: number;\n visitedNodes: string[]; // path taken so far\n}\n\n// ── Routing table ──────────────────────────────────────────────────\nconst routingTable = new Map<string, RouteEntry>();\nconst MAX_ROUTE_TTL = 10;\nconst ROUTE_PRUNE_INTERVAL_MS = 60_000;\nconst ROUTE_STALE_MS = 300_000; // 5 minutes\nlet routeBroadcastInterval: ReturnType<typeof setInterval> | null = null;\n\n/** Get all routing table entries */\nexport function getRoutingTable(): RouteEntry[] {\n return Array.from(routingTable.values());\n}\n\n/**\n * Find the next-hop node to reach a destination.\n *\n * Validates two conditions before returning a route:\n * 1. The route hasn't been idle longer than ROUTE_STALE_MS.\n * 2. The next-hop neighbour still has a live WebSocket (a peer can\n * disconnect without the staleness clock firing — fix for the\n * stale-route bug where mesh kept forwarding through a hop whose\n * socket had already closed, manifesting as 60s timeouts on every\n * task until the heartbeat eventually evicted the entry).\n *\n * On either failure, the entry is removed from the routing table so the\n * caller falls back to a fresh route discovery cycle.\n */\nexport function findNextHop(destinationNodeId: string): string | null {\n const entry = routingTable.get(destinationNodeId);\n if (!entry) return null;\n // Check if route is stale (lastUsedAt-based timeout)\n if (Date.now() - entry.lastUsedAt > ROUTE_STALE_MS) {\n routingTable.delete(destinationNodeId);\n return null;\n }\n // Connection-state validation: the next-hop's WebSocket must be live.\n // Without this check, a closed socket sat in routingTable until staleness\n // expired (5 min default) and routeMessageMultiHop would try to send to\n // a dead peer via sendToPeer — which silently returns false but the\n // pending request still hung waiting for a reply.\n const nextHopWs = peerConnections.get(entry.nextHopNodeId);\n if (!nextHopWs || nextHopWs.readyState !== WebSocket.OPEN) {\n routingTable.delete(destinationNodeId);\n logger.debug(COMPONENT, `Pruned route to ${destinationNodeId} via dead next-hop ${entry.nextHopNodeId}`);\n return null;\n }\n return entry.nextHopNodeId;\n}\n\n/**\n * Invalidate every routing-table entry that flows through a now-disconnected\n * next-hop, then trigger an immediate distance-vector advertisement so\n * remaining peers update their tables before the next periodic broadcast.\n *\n * Called from the WebSocket close + error handlers in both directions\n * (outbound `connectToPeer` and inbound `handleMeshWebSocket`). Without\n * this, a peer drop would leave stale entries in place for up to\n * ROUTE_STALE_MS (5 min) and routes would be pruned only lazily on the\n * next findNextHop call rather than proactively.\n */\nfunction invalidateRoutesVia(disconnectedNodeId: string): number {\n let removed = 0;\n for (const [dest, entry] of routingTable) {\n if (entry.nextHopNodeId === disconnectedNodeId || dest === disconnectedNodeId) {\n routingTable.delete(dest);\n removed++;\n }\n }\n if (removed > 0) {\n logger.info(COMPONENT, `Mesh peer ${disconnectedNodeId} dropped — invalidated ${removed} route(s); broadcasting refresh`);\n // Fire-and-forget: notify remaining peers so their distance-vector\n // tables converge faster than the next periodic broadcast cycle.\n try {\n broadcastRouteAdvertisement();\n } catch (err) {\n logger.debug(COMPONENT, `Route refresh broadcast failed: ${(err as Error).message}`);\n }\n }\n return removed;\n}\n\n/** Update or insert a routing table entry */\nfunction upsertRoute(entry: RouteEntry): void {\n const existing = routingTable.get(entry.destinationNodeId);\n if (!existing || entry.cost < existing.cost) {\n routingTable.set(entry.destinationNodeId, entry);\n } else if (entry.destinationNodeId === entry.nextHopNodeId) {\n // Always prefer direct routes even if cost is equal\n routingTable.set(entry.destinationNodeId, entry);\n }\n}\n\n/** Prune stale routes */\nfunction pruneStaleRoutes(): void {\n const cutoff = Date.now() - ROUTE_STALE_MS;\n for (const [dest, entry] of routingTable) {\n if (entry.lastUsedAt < cutoff) {\n routingTable.delete(dest);\n logger.debug(COMPONENT, `Pruned stale route to ${dest}`);\n }\n }\n}\n\n/** Handle incoming route broadcast advertisements */\nfunction handleRouteBroadcast(msg: MeshMessage, fromNodeId: string): void {\n if (msg.action !== 'route_broadcast' || !msg.payload.advertisements) return;\n\n const ads = msg.payload.advertisements as RouteAdvertisement[];\n const localNodeId = getOrCreateNodeId();\n let updated = false;\n\n for (const ad of ads) {\n // Skip if this node is the destination\n if (ad.destinationNodeId === localNodeId) continue;\n\n // Skip if we've already seen a better path\n const newCost = ad.cost + 1;\n const existing = routingTable.get(ad.destinationNodeId);\n if (existing && existing.cost <= newCost) continue;\n\n // Skip if path would create a loop (our nodeId in visited list)\n if (ad.visitedNodes.includes(localNodeId)) continue;\n\n upsertRoute({\n destinationNodeId: ad.destinationNodeId,\n nextHopNodeId: fromNodeId,\n cost: newCost,\n discoveredAt: Date.now(),\n lastUsedAt: Date.now(),\n });\n updated = true;\n }\n\n if (updated) {\n logger.debug(COMPONENT, `Updated routing table from broadcast by ${fromNodeId}, table size: ${routingTable.size}`);\n }\n}\n\n/** Broadcast our routing table to all connected peers (distance-vector) */\nfunction broadcastRouteAdvertisement(): void {\n const localNodeId = getOrCreateNodeId();\n const advertisements: RouteAdvertisement[] = [];\n\n // Advertise ourselves\n advertisements.push({\n destinationNodeId: localNodeId,\n cost: 0,\n visitedNodes: [localNodeId],\n });\n\n // Advertise routes we know about\n for (const [, entry] of routingTable) {\n advertisements.push({\n destinationNodeId: entry.destinationNodeId,\n cost: entry.cost,\n visitedNodes: [localNodeId],\n });\n }\n\n const msg: MeshMessage = {\n type: 'mesh',\n action: 'route_broadcast',\n fromNodeId: localNodeId,\n payload: { advertisements },\n timestamp: new Date().toISOString(),\n };\n broadcastToMesh(msg);\n}\n\n/** Start route broadcast interval */\nexport function startRouteBroadcast(intervalMs = 30_000): void {\n if (routeBroadcastInterval) return;\n broadcastRouteAdvertisement(); // Immediate initial broadcast\n routeBroadcastInterval = setInterval(() => {\n // Prune stale routes first\n pruneStaleRoutes();\n broadcastRouteAdvertisement();\n }, intervalMs);\n logger.debug(COMPONENT, `Route broadcast started (${Math.round(intervalMs / 1000)}s)`);\n}\n\nexport function stopRouteBroadcast(): void {\n if (routeBroadcastInterval) {\n clearInterval(routeBroadcastInterval);\n routeBroadcastInterval = null;\n }\n routingTable.clear();\n}\n\n/** Route a message through the mesh using the routing table (multi-hop) */\nexport function routeMessageMultiHop(\n destinationNodeId: string,\n message: MeshMessage,\n): boolean {\n const localNodeId = getOrCreateNodeId();\n\n // Already at destination\n if (destinationNodeId === localNodeId) return false;\n\n // Direct connection — send straight there\n if (peerConnections.has(destinationNodeId)) {\n return sendToPeer(destinationNodeId, message);\n }\n\n // Multi-hop: find next hop\n const nextHop = findNextHop(destinationNodeId);\n if (!nextHop) {\n logger.warn(COMPONENT, `No route to ${destinationNodeId}`);\n return false;\n }\n\n // Check TTL\n message.ttl = (message.ttl ?? MAX_ROUTE_TTL) - 1;\n if (message.ttl <= 0) {\n logger.warn(COMPONENT, `TTL expired for message to ${destinationNodeId}`);\n return false;\n }\n\n // Loop detection\n message.visitedNodes = [...(message.visitedNodes || []), localNodeId];\n if (message.visitedNodes.includes(nextHop)) {\n logger.warn(COMPONENT, `Loop detected: ${nextHop} already visited for dest ${destinationNodeId}`);\n return false;\n }\n\n message.hopCount = (message.hopCount || 0) + 1;\n\n // Forward to next hop\n const sent = sendToPeer(nextHop, message);\n if (sent) {\n // Update lastUsed\n const entry = routingTable.get(destinationNodeId);\n if (entry) entry.lastUsedAt = Date.now();\n logger.debug(COMPONENT, `Forwarded to ${nextHop} for ${destinationNodeId} (hop ${message.hopCount})`);\n }\n return sent;\n}\n\n/** Generate HMAC auth token for mesh handshake */\nexport function generateMeshAuth(nodeId: string, meshSecret: string): string {\n const ts = Math.floor(Date.now() / 30000).toString(); // 30-second window\n return createHmac('sha256', meshSecret).update(ts + nodeId).digest('hex');\n}\n\n/** Verify HMAC auth token */\nexport function verifyMeshAuth(token: string, nodeId: string, meshSecret: string): boolean {\n const now = Math.floor(Date.now() / 30000);\n // Check current and previous window to handle clock skew\n for (const ts of [now.toString(), (now - 1).toString()]) {\n const expected = createHmac('sha256', meshSecret).update(ts + nodeId).digest('hex');\n if (token.length === expected.length && timingSafeEqual(Buffer.from(token), Buffer.from(expected))) return true;\n }\n return false;\n}\n\n/** Connect to a peer via WebSocket */\nexport async function connectToPeer(\n address: string,\n port: number,\n localNodeId: string,\n meshSecret: string,\n): Promise<boolean> {\n const peerKey = `${address}:${port}`;\n\n return new Promise((resolve) => {\n const auth = generateMeshAuth(localNodeId, meshSecret);\n // Try wss:// first (TITAN auto-HTTPS via mkcert), fall back to ws://\n const wsUrl = `ws://${address}:${port}?mesh=true&nodeId=${localNodeId}&auth=${auth}`;\n const wssUrl = `wss://${address}:${port}?mesh=true&nodeId=${localNodeId}&auth=${auth}`;\n const url = peerUseTls.get(peerKey) ? wssUrl : wsUrl;\n\n const ws = new WebSocket(url, { handshakeTimeout: 5000 });\n let remoteNodeId: string | null = null;\n let resolved = false;\n\n ws.on('open', () => {\n logger.info(COMPONENT, `Connected to peer at ${address}:${port}`);\n // Reset reconnect state on successful connection\n const existing = reconnectState.get(peerKey);\n if (existing?.timer) clearTimeout(existing.timer);\n reconnectState.delete(peerKey);\n });\n\n ws.on('message', (data) => {\n try {\n const msg = JSON.parse(data.toString()) as MeshMessage;\n if (msg.type !== 'mesh') return;\n\n if (msg.action === 'heartbeat' && msg.fromNodeId) {\n remoteNodeId = msg.fromNodeId;\n peerConnections.set(remoteNodeId, ws);\n registerPeer({\n nodeId: remoteNodeId,\n hostname: (msg.payload?.hostname as string) || address,\n address,\n port,\n version: (msg.payload?.version as string) || 'unknown',\n models: (msg.payload?.models as string[]) || [],\n agentCount: (msg.payload?.agentCount as number) || 0,\n load: (msg.payload?.load as number) || 0,\n discoveredVia: 'manual',\n lastSeen: Date.now(),\n });\n if (!resolved) { resolved = true; resolve(true); }\n }\n\n if (msg.action === 'task_response' && msg.requestId) {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(msg.requestId);\n pending.resolve(msg.payload);\n }\n }\n } catch {\n // Ignore malformed messages\n }\n });\n\n ws.on('error', () => {\n if (!resolved) { resolved = true; resolve(false); }\n });\n\n ws.on('close', () => {\n if (remoteNodeId) {\n peerConnections.delete(remoteNodeId);\n // Reject any pending requests targeted at this peer\n for (const [reqId, req] of pendingRequests) {\n if (req.peerNodeId === remoteNodeId) {\n clearTimeout(req.timeout);\n req.reject(new Error(`Peer disconnected: ${remoteNodeId}`));\n pendingRequests.delete(reqId);\n }\n }\n // Invalidate any routes that traversed this peer + broadcast\n // refresh so other nodes converge faster than the periodic cycle.\n invalidateRoutesVia(remoteNodeId);\n logger.info(COMPONENT, `Peer disconnected: ${remoteNodeId}`);\n }\n if (!resolved) { resolved = true; resolve(false); }\n\n // ── Reconnect with exponential backoff + jitter ──\n const existing = reconnectState.get(peerKey);\n if (existing && existing.address === address && existing.port === port) {\n // Existing reconnect state with matching address — bump attempt counter\n existing.attempts++;\n } else if (existing) {\n // Address changed (e.g., new mDNS/Tailscale discovery) — reset\n if (existing.timer) clearTimeout(existing.timer);\n reconnectState.set(peerKey, { attempts: 1, nodeId: remoteNodeId || existing.nodeId, address, port, meshSecret });\n } else {\n reconnectState.set(peerKey, { attempts: 1, nodeId: remoteNodeId || 'unknown', address, port, meshSecret });\n }\n\n const state = reconnectState.get(peerKey)!;\n const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(2, state.attempts - 1), RECONNECT_MAX_DELAY);\n const jitter = delay * RECONNECT_JITTER_FRAC * (Math.random() * 2 - 1);\n const jitteredDelay = Math.max(100, Math.round(delay + jitter));\n\n logger.info(COMPONENT, `Reconnecting to ${address}:${port} in ${jitteredDelay}ms (attempt ${state.attempts})`);\n\n const timer = setTimeout(() => {\n connectToPeer(address, port, localNodeId, meshSecret).catch(e => logger.debug(COMPONENT, `Background reconnect failed: ${(e as Error).message}`));\n }, jitteredDelay);\n state.timer = timer;\n });\n\n // Timeout\n setTimeout(() => {\n if (ws.readyState !== WebSocket.OPEN) {\n ws.close();\n if (!resolved) { resolved = true; resolve(false); }\n }\n }, 5000);\n });\n}\n\n/** Send a message to a specific peer */\nexport function sendToPeer(nodeId: string, message: MeshMessage): boolean {\n const ws = peerConnections.get(nodeId);\n if (!ws || ws.readyState !== WebSocket.OPEN) return false;\n ws.send(JSON.stringify(message));\n return true;\n}\n\n/** Broadcast a message to all connected peers */\nexport function broadcastToMesh(message: MeshMessage): void {\n const data = JSON.stringify(message);\n for (const [nodeId, ws] of peerConnections) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(data);\n } else {\n peerConnections.delete(nodeId);\n }\n }\n}\n\n/** Route a task to a remote node and await the response (supports multi-hop) */\nexport function routeTaskToNode(\n nodeId: string,\n requestId: string,\n message: string,\n model: string,\n timeoutMs = 60_000,\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingRequests.delete(requestId);\n reject(new Error(`Mesh task timed out (peer: ${nodeId})`));\n }, timeoutMs);\n\n pendingRequests.set(requestId, { resolve, reject, timeout, peerNodeId: nodeId });\n\n // First try direct send\n let sent = sendToPeer(nodeId, {\n type: 'mesh',\n action: 'task_request',\n fromNodeId: getOrCreateNodeId(),\n toNodeId: nodeId,\n requestId,\n payload: { message, model },\n timestamp: new Date().toISOString(),\n });\n\n // If no direct connection, try multi-hop routing\n if (!sent) {\n sent = routeMessageMultiHop(nodeId, {\n type: 'mesh',\n action: 'route_forward',\n fromNodeId: getOrCreateNodeId(),\n toNodeId: nodeId,\n requestId,\n payload: {\n innerAction: 'task_request',\n message,\n model,\n },\n timestamp: new Date().toISOString(),\n });\n }\n\n if (!sent) {\n clearTimeout(timeout);\n pendingRequests.delete(requestId);\n reject(new Error(`Cannot reach peer: ${nodeId} (no direct or multi-hop route)`));\n }\n });\n}\n\n/** Handle an incoming mesh WebSocket connection (called from gateway) */\nexport function handleMeshWebSocket(\n ws: WebSocket,\n nodeId: string,\n localNodeId: string,\n onTaskRequest?: (msg: MeshMessage, reply: (payload: Record<string, unknown>) => void) => void,\n): void {\n peerConnections.set(nodeId, ws);\n logger.info(COMPONENT, `Mesh peer connected: ${nodeId}`);\n\n ws.on('message', (data) => {\n try {\n const msg = JSON.parse(data.toString()) as MeshMessage;\n if (msg.type !== 'mesh') return;\n\n if (msg.action === 'heartbeat') {\n registerPeer({\n nodeId: msg.fromNodeId,\n hostname: (msg.payload?.hostname as string) || 'unknown',\n address: '', // Already connected\n port: 0,\n version: (msg.payload?.version as string) || 'unknown',\n models: (msg.payload?.models as string[]) || [],\n agentCount: (msg.payload?.agentCount as number) || 0,\n load: (msg.payload?.load as number) || 0,\n discoveredVia: 'manual',\n lastSeen: Date.now(),\n });\n }\n\n if (msg.action === 'task_request' && onTaskRequest && msg.requestId) {\n activeRemoteTasks++;\n let replied = false;\n const sendReply = (payload: Record<string, unknown>) => {\n if (replied) return;\n replied = true;\n activeRemoteTasks = Math.max(0, activeRemoteTasks - 1);\n const reply: MeshMessage = {\n type: 'mesh',\n action: 'task_response',\n fromNodeId: localNodeId,\n toNodeId: msg.fromNodeId,\n requestId: msg.requestId,\n payload,\n timestamp: new Date().toISOString(),\n };\n if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(reply));\n };\n try {\n onTaskRequest(msg, sendReply);\n } catch (err) {\n sendReply({ error: `Handler error: ${(err as Error).message}` });\n }\n }\n\n if (msg.action === 'task_response' && msg.requestId) {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(msg.requestId);\n pending.resolve(msg.payload);\n }\n }\n\n // Handle route broadcast advertisements (multi-hop routing)\n if (msg.action === 'route_broadcast') {\n handleRouteBroadcast(msg, nodeId);\n }\n\n // Handle forwarded messages (multi-hop routing)\n if (msg.action === 'route_forward' && msg.toNodeId) {\n const localNodeId = getOrCreateNodeId();\n // Forwarded message arrived — if we are the destination, process it, otherwise re-forward\n if (msg.toNodeId === localNodeId) {\n // This is for us — treat as the inner message\n const innerAction = (msg.payload.innerAction as MeshMessage['action']) || 'task_request';\n if (innerAction === 'task_request' && onTaskRequest && msg.requestId) {\n activeRemoteTasks++;\n let replied = false;\n\n // The original requester's nodeId is `msg.fromNodeId`. Pin it\n // so the closure has a stable reference even if msg is mutated\n // by the handler. Carry it through the response metadata so\n // intermediate hops can route based on the requester id, not\n // just whichever socket the inbound message came in on.\n const originalRequesterId = msg.fromNodeId;\n const originalRequestId = msg.requestId;\n\n const sendReply = (payload: Record<string, unknown>) => {\n if (replied) return;\n replied = true;\n activeRemoteTasks = Math.max(0, activeRemoteTasks - 1);\n const reply: MeshMessage = {\n type: 'mesh',\n action: 'task_response',\n fromNodeId: localNodeId,\n toNodeId: originalRequesterId,\n requestId: originalRequestId,\n payload: { ...payload, originalRequesterId },\n timestamp: new Date().toISOString(),\n };\n\n // Mesh task 1 fix: don't blindly reply on the inbound\n // socket. The forwarded request may have arrived via an\n // intermediate hop (A → B → C, with us being C); the\n // socket we received it on is the link to B, not A.\n // Replying via `ws.send` would deliver to B and B would\n // have no way to forward it back to A unless B also\n // happens to have a route — which in practice means the\n // reply gets dropped at B because B's task_response handler\n // only resolves pending requests it owns.\n //\n // Use routeMessageMultiHop, which: (1) sends directly if\n // the requester is one of our peers, (2) otherwise looks\n // up the next hop in our routing table, (3) otherwise\n // falls back to the inbound socket as a last resort.\n if (peerConnections.has(originalRequesterId)) {\n sendToPeer(originalRequesterId, reply);\n return;\n }\n const routed = routeMessageMultiHop(originalRequesterId, {\n ...reply,\n action: 'route_forward',\n payload: {\n innerAction: 'task_response',\n originalRequesterId,\n ...payload,\n },\n });\n if (!routed && ws.readyState === WebSocket.OPEN) {\n // Last-ditch: send back along the inbound socket so the\n // intermediate hop can at least see + log the response.\n ws.send(JSON.stringify(reply));\n }\n };\n try {\n onTaskRequest({ ...msg, action: 'task_request' }, sendReply);\n } catch (err) {\n sendReply({ error: `Handler error: ${(err as Error).message}` });\n }\n } else if (innerAction === 'task_response' && msg.requestId) {\n // We're the original requester for a multi-hop response that\n // arrived back via route_forward — resolve the pending request.\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(msg.requestId);\n pending.resolve(msg.payload);\n }\n }\n } else {\n // Not for us — forward along the route\n routeMessageMultiHop(msg.toNodeId, msg);\n }\n }\n } catch {\n // Ignore\n }\n });\n\n const cleanup = (cause: 'close' | 'error') => {\n peerConnections.delete(nodeId);\n // Reject any pending requests for this peer\n for (const [reqId, req] of pendingRequests) {\n if (req.peerNodeId === nodeId) {\n clearTimeout(req.timeout);\n req.reject(new Error(`Peer ${cause}: ${nodeId}`));\n pendingRequests.delete(reqId);\n }\n }\n // Mesh task 2: invalidate routes that traversed this peer + broadcast\n // refresh so the rest of the mesh converges within seconds, not the\n // 5-min ROUTE_STALE_MS lazy-prune window.\n invalidateRoutesVia(nodeId);\n logger.info(COMPONENT, `Mesh peer disconnected (${cause}): ${nodeId}`);\n };\n\n ws.on('close', () => cleanup('close'));\n ws.on('error', () => cleanup('error'));\n}\n\n/** Start sending periodic heartbeats to all connected peers */\nexport function startHeartbeat(\n localNodeId: string,\n payload?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>),\n intervalMs = 60_000,\n): void {\n if (heartbeatInterval) return; // Already running\n heartbeatInterval = setInterval(async () => {\n const resolved = typeof payload === 'function' ? await payload() : (payload || {});\n const msg: MeshMessage = {\n type: 'mesh',\n action: 'heartbeat',\n fromNodeId: localNodeId,\n payload: resolved,\n timestamp: new Date().toISOString(),\n };\n broadcastToMesh(msg);\n }, intervalMs);\n logger.debug(COMPONENT, `Heartbeat interval started (${Math.round(intervalMs / 1000)}s)`);\n}\n\n/** Stop the heartbeat interval */\nexport function stopHeartbeat(): void {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval);\n heartbeatInterval = null;\n logger.debug(COMPONENT, 'Heartbeat interval stopped');\n }\n}\n\n/** Get connected peer count */\nexport function getConnectedPeerCount(): number {\n let count = 0;\n for (const [, ws] of peerConnections) {\n if (ws.readyState === WebSocket.OPEN) count++;\n }\n return count;\n}\n\n/** Disconnect a specific peer */\nexport function disconnectPeer(nodeId: string): void {\n const ws = peerConnections.get(nodeId);\n if (ws) {\n ws.close();\n peerConnections.delete(nodeId);\n // Reject any pending requests\n for (const [reqId, req] of pendingRequests) {\n if (req.peerNodeId === nodeId) {\n clearTimeout(req.timeout);\n req.reject(new Error(`Peer disconnected: ${nodeId}`));\n pendingRequests.delete(reqId);\n }\n }\n logger.info(COMPONENT, `Peer disconnected (requested): ${nodeId}`);\n }\n}\n\n/** Disconnect all peers */\nexport function disconnectAll(): void {\n stopHeartbeat();\n stopRouteBroadcast();\n for (const [nodeId, ws] of peerConnections) {\n ws.close();\n peerConnections.delete(nodeId);\n }\n for (const [id, req] of pendingRequests) {\n clearTimeout(req.timeout);\n req.reject(new Error('Mesh shutting down'));\n pendingRequests.delete(id);\n }\n reconnectState.clear();\n}\n"],"mappings":";AAKA,SAAS,iBAAiB;AAC1B,SAAS,YAAY,uBAAuB;AAC5C,OAAO,YAAY;AACnB,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAElC,MAAM,YAAY;AAGlB,MAAM,kBAAkB,oBAAI,IAAuB;AACnD,MAAM,kBAAkB,oBAAI,IAAwI;AACpK,MAAM,iBAAiB,oBAAI,IAA4I;AACvK,MAAM,aAAa,oBAAI,IAAqB;AAU5C,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAC5B,MAAM,wBAAwB;AAC9B,MAAM,iCAAiC;AAGhC,SAAS,WAAW,SAAiB,MAAc,KAAoB;AAC1E,aAAW,IAAI,GAAG,OAAO,IAAI,IAAI,IAAI,GAAG;AAC5C;AAMO,SAAS,kBACZ,YACA,SACA,YACA,SACI;AACJ,QAAM,SAAS,GAAG,UAAU,IAAI,OAAO;AACvC,QAAM,SAAS,GAAG,UAAU,IAAI,OAAO;AACvC,QAAM,QAAQ,eAAe,IAAI,MAAM;AACvC,MAAI,UAAU,eAAe,cAAc,YAAY,UAAU;AAC7D,QAAI,MAAM,MAAO,cAAa,MAAM,KAAK;AACzC,mBAAe,IAAI,QAAQ,EAAE,GAAG,OAAO,SAAS,YAAY,MAAM,QAAQ,CAAC;AAC3E,mBAAe,OAAO,MAAM;AAC5B,WAAO,MAAM,WAAW,yBAAyB,MAAM,WAAM,MAAM,EAAE;AAAA,EACzE;AACJ;AAEA,IAAI,oBAA2D;AAC/D,IAAI,oBAAoB;AAGjB,SAAS,2BAAmC;AAC/C,SAAO;AACX;AAoCA,MAAM,eAAe,oBAAI,IAAwB;AACjD,MAAM,gBAAgB;AACtB,MAAM,0BAA0B;AAChC,MAAM,iBAAiB;AACvB,IAAI,yBAAgE;AAG7D,SAAS,kBAAgC;AAC5C,SAAO,MAAM,KAAK,aAAa,OAAO,CAAC;AAC3C;AAgBO,SAAS,YAAY,mBAA0C;AAClE,QAAM,QAAQ,aAAa,IAAI,iBAAiB;AAChD,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,KAAK,IAAI,IAAI,MAAM,aAAa,gBAAgB;AAChD,iBAAa,OAAO,iBAAiB;AACrC,WAAO;AAAA,EACX;AAMA,QAAM,YAAY,gBAAgB,IAAI,MAAM,aAAa;AACzD,MAAI,CAAC,aAAa,UAAU,eAAe,UAAU,MAAM;AACvD,iBAAa,OAAO,iBAAiB;AACrC,WAAO,MAAM,WAAW,mBAAmB,iBAAiB,sBAAsB,MAAM,aAAa,EAAE;AACvG,WAAO;AAAA,EACX;AACA,SAAO,MAAM;AACjB;AAaA,SAAS,oBAAoB,oBAAoC;AAC7D,MAAI,UAAU;AACd,aAAW,CAAC,MAAM,KAAK,KAAK,cAAc;AACtC,QAAI,MAAM,kBAAkB,sBAAsB,SAAS,oBAAoB;AAC3E,mBAAa,OAAO,IAAI;AACxB;AAAA,IACJ;AAAA,EACJ;AACA,MAAI,UAAU,GAAG;AACb,WAAO,KAAK,WAAW,aAAa,kBAAkB,+BAA0B,OAAO,iCAAiC;AAGxH,QAAI;AACA,kCAA4B;AAAA,IAChC,SAAS,KAAK;AACV,aAAO,MAAM,WAAW,mCAAoC,IAAc,OAAO,EAAE;AAAA,IACvF;AAAA,EACJ;AACA,SAAO;AACX;AAGA,SAAS,YAAY,OAAyB;AAC1C,QAAM,WAAW,aAAa,IAAI,MAAM,iBAAiB;AACzD,MAAI,CAAC,YAAY,MAAM,OAAO,SAAS,MAAM;AACzC,iBAAa,IAAI,MAAM,mBAAmB,KAAK;AAAA,EACnD,WAAW,MAAM,sBAAsB,MAAM,eAAe;AAExD,iBAAa,IAAI,MAAM,mBAAmB,KAAK;AAAA,EACnD;AACJ;AAGA,SAAS,mBAAyB;AAC9B,QAAM,SAAS,KAAK,IAAI,IAAI;AAC5B,aAAW,CAAC,MAAM,KAAK,KAAK,cAAc;AACtC,QAAI,MAAM,aAAa,QAAQ;AAC3B,mBAAa,OAAO,IAAI;AACxB,aAAO,MAAM,WAAW,yBAAyB,IAAI,EAAE;AAAA,IAC3D;AAAA,EACJ;AACJ;AAGA,SAAS,qBAAqB,KAAkB,YAA0B;AACtE,MAAI,IAAI,WAAW,qBAAqB,CAAC,IAAI,QAAQ,eAAgB;AAErE,QAAM,MAAM,IAAI,QAAQ;AACxB,QAAM,cAAc,kBAAkB;AACtC,MAAI,UAAU;AAEd,aAAW,MAAM,KAAK;AAElB,QAAI,GAAG,sBAAsB,YAAa;AAG1C,UAAM,UAAU,GAAG,OAAO;AAC1B,UAAM,WAAW,aAAa,IAAI,GAAG,iBAAiB;AACtD,QAAI,YAAY,SAAS,QAAQ,QAAS;AAG1C,QAAI,GAAG,aAAa,SAAS,WAAW,EAAG;AAE3C,gBAAY;AAAA,MACR,mBAAmB,GAAG;AAAA,MACtB,eAAe;AAAA,MACf,MAAM;AAAA,MACN,cAAc,KAAK,IAAI;AAAA,MACvB,YAAY,KAAK,IAAI;AAAA,IACzB,CAAC;AACD,cAAU;AAAA,EACd;AAEA,MAAI,SAAS;AACT,WAAO,MAAM,WAAW,2CAA2C,UAAU,iBAAiB,aAAa,IAAI,EAAE;AAAA,EACrH;AACJ;AAGA,SAAS,8BAAoC;AACzC,QAAM,cAAc,kBAAkB;AACtC,QAAM,iBAAuC,CAAC;AAG9C,iBAAe,KAAK;AAAA,IAChB,mBAAmB;AAAA,IACnB,MAAM;AAAA,IACN,cAAc,CAAC,WAAW;AAAA,EAC9B,CAAC;AAGD,aAAW,CAAC,EAAE,KAAK,KAAK,cAAc;AAClC,mBAAe,KAAK;AAAA,MAChB,mBAAmB,MAAM;AAAA,MACzB,MAAM,MAAM;AAAA,MACZ,cAAc,CAAC,WAAW;AAAA,IAC9B,CAAC;AAAA,EACL;AAEA,QAAM,MAAmB;AAAA,IACrB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS,EAAE,eAAe;AAAA,IAC1B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AACA,kBAAgB,GAAG;AACvB;AAGO,SAAS,oBAAoB,aAAa,KAAc;AAC3D,MAAI,uBAAwB;AAC5B,8BAA4B;AAC5B,2BAAyB,YAAY,MAAM;AAEvC,qBAAiB;AACjB,gCAA4B;AAAA,EAChC,GAAG,UAAU;AACb,SAAO,MAAM,WAAW,4BAA4B,KAAK,MAAM,aAAa,GAAI,CAAC,IAAI;AACzF;AAEO,SAAS,qBAA2B;AACvC,MAAI,wBAAwB;AACxB,kBAAc,sBAAsB;AACpC,6BAAyB;AAAA,EAC7B;AACA,eAAa,MAAM;AACvB;AAGO,SAAS,qBACZ,mBACA,SACO;AACP,QAAM,cAAc,kBAAkB;AAGtC,MAAI,sBAAsB,YAAa,QAAO;AAG9C,MAAI,gBAAgB,IAAI,iBAAiB,GAAG;AACxC,WAAO,WAAW,mBAAmB,OAAO;AAAA,EAChD;AAGA,QAAM,UAAU,YAAY,iBAAiB;AAC7C,MAAI,CAAC,SAAS;AACV,WAAO,KAAK,WAAW,eAAe,iBAAiB,EAAE;AACzD,WAAO;AAAA,EACX;AAGA,UAAQ,OAAO,QAAQ,OAAO,iBAAiB;AAC/C,MAAI,QAAQ,OAAO,GAAG;AAClB,WAAO,KAAK,WAAW,8BAA8B,iBAAiB,EAAE;AACxE,WAAO;AAAA,EACX;AAGA,UAAQ,eAAe,CAAC,GAAI,QAAQ,gBAAgB,CAAC,GAAI,WAAW;AACpE,MAAI,QAAQ,aAAa,SAAS,OAAO,GAAG;AACxC,WAAO,KAAK,WAAW,kBAAkB,OAAO,6BAA6B,iBAAiB,EAAE;AAChG,WAAO;AAAA,EACX;AAEA,UAAQ,YAAY,QAAQ,YAAY,KAAK;AAG7C,QAAM,OAAO,WAAW,SAAS,OAAO;AACxC,MAAI,MAAM;AAEN,UAAM,QAAQ,aAAa,IAAI,iBAAiB;AAChD,QAAI,MAAO,OAAM,aAAa,KAAK,IAAI;AACvC,WAAO,MAAM,WAAW,gBAAgB,OAAO,QAAQ,iBAAiB,SAAS,QAAQ,QAAQ,GAAG;AAAA,EACxG;AACA,SAAO;AACX;AAGO,SAAS,iBAAiB,QAAgB,YAA4B;AACzE,QAAM,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAK,EAAE,SAAS;AACnD,SAAO,WAAW,UAAU,UAAU,EAAE,OAAO,KAAK,MAAM,EAAE,OAAO,KAAK;AAC5E;AAGO,SAAS,eAAe,OAAe,QAAgB,YAA6B;AACvF,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAK;AAEzC,aAAW,MAAM,CAAC,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAAC,GAAG;AACrD,UAAM,WAAW,WAAW,UAAU,UAAU,EAAE,OAAO,KAAK,MAAM,EAAE,OAAO,KAAK;AAClF,QAAI,MAAM,WAAW,SAAS,UAAU,gBAAgB,OAAO,KAAK,KAAK,GAAG,OAAO,KAAK,QAAQ,CAAC,EAAG,QAAO;AAAA,EAC/G;AACA,SAAO;AACX;AAGA,eAAsB,cAClB,SACA,MACA,aACA,YACgB;AAChB,QAAM,UAAU,GAAG,OAAO,IAAI,IAAI;AAElC,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,UAAM,OAAO,iBAAiB,aAAa,UAAU;AAErD,UAAM,QAAQ,QAAQ,OAAO,IAAI,IAAI,qBAAqB,WAAW,SAAS,IAAI;AAClF,UAAM,SAAS,SAAS,OAAO,IAAI,IAAI,qBAAqB,WAAW,SAAS,IAAI;AACpF,UAAM,MAAM,WAAW,IAAI,OAAO,IAAI,SAAS;AAE/C,UAAM,KAAK,IAAI,UAAU,KAAK,EAAE,kBAAkB,IAAK,CAAC;AACxD,QAAI,eAA8B;AAClC,QAAI,WAAW;AAEf,OAAG,GAAG,QAAQ,MAAM;AAChB,aAAO,KAAK,WAAW,wBAAwB,OAAO,IAAI,IAAI,EAAE;AAEhE,YAAM,WAAW,eAAe,IAAI,OAAO;AAC3C,UAAI,UAAU,MAAO,cAAa,SAAS,KAAK;AAChD,qBAAe,OAAO,OAAO;AAAA,IACjC,CAAC;AAED,OAAG,GAAG,WAAW,CAAC,SAAS;AACvB,UAAI;AACA,cAAM,MAAM,KAAK,MAAM,KAAK,SAAS,CAAC;AACtC,YAAI,IAAI,SAAS,OAAQ;AAEzB,YAAI,IAAI,WAAW,eAAe,IAAI,YAAY;AAC9C,yBAAe,IAAI;AACnB,0BAAgB,IAAI,cAAc,EAAE;AACpC,uBAAa;AAAA,YACT,QAAQ;AAAA,YACR,UAAW,IAAI,SAAS,YAAuB;AAAA,YAC/C;AAAA,YACA;AAAA,YACA,SAAU,IAAI,SAAS,WAAsB;AAAA,YAC7C,QAAS,IAAI,SAAS,UAAuB,CAAC;AAAA,YAC9C,YAAa,IAAI,SAAS,cAAyB;AAAA,YACnD,MAAO,IAAI,SAAS,QAAmB;AAAA,YACvC,eAAe;AAAA,YACf,UAAU,KAAK,IAAI;AAAA,UACvB,CAAC;AACD,cAAI,CAAC,UAAU;AAAE,uBAAW;AAAM,oBAAQ,IAAI;AAAA,UAAG;AAAA,QACrD;AAEA,YAAI,IAAI,WAAW,mBAAmB,IAAI,WAAW;AACjD,gBAAM,UAAU,gBAAgB,IAAI,IAAI,SAAS;AACjD,cAAI,SAAS;AACT,yBAAa,QAAQ,OAAO;AAC5B,4BAAgB,OAAO,IAAI,SAAS;AACpC,oBAAQ,QAAQ,IAAI,OAAO;AAAA,UAC/B;AAAA,QACJ;AAAA,MACJ,QAAQ;AAAA,MAER;AAAA,IACJ,CAAC;AAED,OAAG,GAAG,SAAS,MAAM;AACjB,UAAI,CAAC,UAAU;AAAE,mBAAW;AAAM,gBAAQ,KAAK;AAAA,MAAG;AAAA,IACtD,CAAC;AAED,OAAG,GAAG,SAAS,MAAM;AACjB,UAAI,cAAc;AACd,wBAAgB,OAAO,YAAY;AAEnC,mBAAW,CAAC,OAAO,GAAG,KAAK,iBAAiB;AACxC,cAAI,IAAI,eAAe,cAAc;AACjC,yBAAa,IAAI,OAAO;AACxB,gBAAI,OAAO,IAAI,MAAM,sBAAsB,YAAY,EAAE,CAAC;AAC1D,4BAAgB,OAAO,KAAK;AAAA,UAChC;AAAA,QACJ;AAGA,4BAAoB,YAAY;AAChC,eAAO,KAAK,WAAW,sBAAsB,YAAY,EAAE;AAAA,MAC/D;AACA,UAAI,CAAC,UAAU;AAAE,mBAAW;AAAM,gBAAQ,KAAK;AAAA,MAAG;AAGlD,YAAM,WAAW,eAAe,IAAI,OAAO;AAC3C,UAAI,YAAY,SAAS,YAAY,WAAW,SAAS,SAAS,MAAM;AAEpE,iBAAS;AAAA,MACb,WAAW,UAAU;AAEjB,YAAI,SAAS,MAAO,cAAa,SAAS,KAAK;AAC/C,uBAAe,IAAI,SAAS,EAAE,UAAU,GAAG,QAAQ,gBAAgB,SAAS,QAAQ,SAAS,MAAM,WAAW,CAAC;AAAA,MACnH,OAAO;AACH,uBAAe,IAAI,SAAS,EAAE,UAAU,GAAG,QAAQ,gBAAgB,WAAW,SAAS,MAAM,WAAW,CAAC;AAAA,MAC7G;AAEA,YAAM,QAAQ,eAAe,IAAI,OAAO;AACxC,YAAM,QAAQ,KAAK,IAAI,uBAAuB,KAAK,IAAI,GAAG,MAAM,WAAW,CAAC,GAAG,mBAAmB;AAClG,YAAM,SAAS,QAAQ,yBAAyB,KAAK,OAAO,IAAI,IAAI;AACpE,YAAM,gBAAgB,KAAK,IAAI,KAAK,KAAK,MAAM,QAAQ,MAAM,CAAC;AAE9D,aAAO,KAAK,WAAW,mBAAmB,OAAO,IAAI,IAAI,OAAO,aAAa,eAAe,MAAM,QAAQ,GAAG;AAE7G,YAAM,QAAQ,WAAW,MAAM;AAC3B,sBAAc,SAAS,MAAM,aAAa,UAAU,EAAE,MAAM,OAAK,OAAO,MAAM,WAAW,gCAAiC,EAAY,OAAO,EAAE,CAAC;AAAA,MACpJ,GAAG,aAAa;AAChB,YAAM,QAAQ;AAAA,IAClB,CAAC;AAGD,eAAW,MAAM;AACb,UAAI,GAAG,eAAe,UAAU,MAAM;AAClC,WAAG,MAAM;AACT,YAAI,CAAC,UAAU;AAAE,qBAAW;AAAM,kBAAQ,KAAK;AAAA,QAAG;AAAA,MACtD;AAAA,IACJ,GAAG,GAAI;AAAA,EACX,CAAC;AACL;AAGO,SAAS,WAAW,QAAgB,SAA+B;AACtE,QAAM,KAAK,gBAAgB,IAAI,MAAM;AACrC,MAAI,CAAC,MAAM,GAAG,eAAe,UAAU,KAAM,QAAO;AACpD,KAAG,KAAK,KAAK,UAAU,OAAO,CAAC;AAC/B,SAAO;AACX;AAGO,SAAS,gBAAgB,SAA4B;AACxD,QAAM,OAAO,KAAK,UAAU,OAAO;AACnC,aAAW,CAAC,QAAQ,EAAE,KAAK,iBAAiB;AACxC,QAAI,GAAG,eAAe,UAAU,MAAM;AAClC,SAAG,KAAK,IAAI;AAAA,IAChB,OAAO;AACH,sBAAgB,OAAO,MAAM;AAAA,IACjC;AAAA,EACJ;AACJ;AAGO,SAAS,gBACZ,QACA,WACA,SACA,OACA,YAAY,KACI;AAChB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACpC,UAAM,UAAU,WAAW,MAAM;AAC7B,sBAAgB,OAAO,SAAS;AAChC,aAAO,IAAI,MAAM,8BAA8B,MAAM,GAAG,CAAC;AAAA,IAC7D,GAAG,SAAS;AAEZ,oBAAgB,IAAI,WAAW,EAAE,SAAS,QAAQ,SAAS,YAAY,OAAO,CAAC;AAG/E,QAAI,OAAO,WAAW,QAAQ;AAAA,MAC1B,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,YAAY,kBAAkB;AAAA,MAC9B,UAAU;AAAA,MACV;AAAA,MACA,SAAS,EAAE,SAAS,MAAM;AAAA,MAC1B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC,CAAC;AAGD,QAAI,CAAC,MAAM;AACP,aAAO,qBAAqB,QAAQ;AAAA,QAChC,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,YAAY,kBAAkB;AAAA,QAC9B,UAAU;AAAA,QACV;AAAA,QACA,SAAS;AAAA,UACL,aAAa;AAAA,UACb;AAAA,UACA;AAAA,QACJ;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACtC,CAAC;AAAA,IACL;AAEA,QAAI,CAAC,MAAM;AACP,mBAAa,OAAO;AACpB,sBAAgB,OAAO,SAAS;AAChC,aAAO,IAAI,MAAM,sBAAsB,MAAM,iCAAiC,CAAC;AAAA,IACnF;AAAA,EACJ,CAAC;AACL;AAGO,SAAS,oBACZ,IACA,QACA,aACA,eACI;AACJ,kBAAgB,IAAI,QAAQ,EAAE;AAC9B,SAAO,KAAK,WAAW,wBAAwB,MAAM,EAAE;AAEvD,KAAG,GAAG,WAAW,CAAC,SAAS;AACvB,QAAI;AACA,YAAM,MAAM,KAAK,MAAM,KAAK,SAAS,CAAC;AACtC,UAAI,IAAI,SAAS,OAAQ;AAEzB,UAAI,IAAI,WAAW,aAAa;AAC5B,qBAAa;AAAA,UACT,QAAQ,IAAI;AAAA,UACZ,UAAW,IAAI,SAAS,YAAuB;AAAA,UAC/C,SAAS;AAAA;AAAA,UACT,MAAM;AAAA,UACN,SAAU,IAAI,SAAS,WAAsB;AAAA,UAC7C,QAAS,IAAI,SAAS,UAAuB,CAAC;AAAA,UAC9C,YAAa,IAAI,SAAS,cAAyB;AAAA,UACnD,MAAO,IAAI,SAAS,QAAmB;AAAA,UACvC,eAAe;AAAA,UACf,UAAU,KAAK,IAAI;AAAA,QACvB,CAAC;AAAA,MACL;AAEA,UAAI,IAAI,WAAW,kBAAkB,iBAAiB,IAAI,WAAW;AACjE;AACA,YAAI,UAAU;AACd,cAAM,YAAY,CAAC,YAAqC;AACpD,cAAI,QAAS;AACb,oBAAU;AACV,8BAAoB,KAAK,IAAI,GAAG,oBAAoB,CAAC;AACrD,gBAAM,QAAqB;AAAA,YACvB,MAAM;AAAA,YACN,QAAQ;AAAA,YACR,YAAY;AAAA,YACZ,UAAU,IAAI;AAAA,YACd,WAAW,IAAI;AAAA,YACf;AAAA,YACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,UACtC;AACA,cAAI,GAAG,eAAe,UAAU,KAAM,IAAG,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,QACvE;AACA,YAAI;AACA,wBAAc,KAAK,SAAS;AAAA,QAChC,SAAS,KAAK;AACV,oBAAU,EAAE,OAAO,kBAAmB,IAAc,OAAO,GAAG,CAAC;AAAA,QACnE;AAAA,MACJ;AAEA,UAAI,IAAI,WAAW,mBAAmB,IAAI,WAAW;AACjD,cAAM,UAAU,gBAAgB,IAAI,IAAI,SAAS;AACjD,YAAI,SAAS;AACT,uBAAa,QAAQ,OAAO;AAC5B,0BAAgB,OAAO,IAAI,SAAS;AACpC,kBAAQ,QAAQ,IAAI,OAAO;AAAA,QAC/B;AAAA,MACJ;AAGA,UAAI,IAAI,WAAW,mBAAmB;AAClC,6BAAqB,KAAK,MAAM;AAAA,MACpC;AAGA,UAAI,IAAI,WAAW,mBAAmB,IAAI,UAAU;AAChD,cAAMA,eAAc,kBAAkB;AAEtC,YAAI,IAAI,aAAaA,cAAa;AAE9B,gBAAM,cAAe,IAAI,QAAQ,eAAyC;AAC1E,cAAI,gBAAgB,kBAAkB,iBAAiB,IAAI,WAAW;AAClE;AACA,gBAAI,UAAU;AAOd,kBAAM,sBAAsB,IAAI;AAChC,kBAAM,oBAAoB,IAAI;AAE9B,kBAAM,YAAY,CAAC,YAAqC;AACpD,kBAAI,QAAS;AACb,wBAAU;AACV,kCAAoB,KAAK,IAAI,GAAG,oBAAoB,CAAC;AACrD,oBAAM,QAAqB;AAAA,gBACvB,MAAM;AAAA,gBACN,QAAQ;AAAA,gBACR,YAAYA;AAAA,gBACZ,UAAU;AAAA,gBACV,WAAW;AAAA,gBACX,SAAS,EAAE,GAAG,SAAS,oBAAoB;AAAA,gBAC3C,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,cACtC;AAgBA,kBAAI,gBAAgB,IAAI,mBAAmB,GAAG;AAC1C,2BAAW,qBAAqB,KAAK;AACrC;AAAA,cACJ;AACA,oBAAM,SAAS,qBAAqB,qBAAqB;AAAA,gBACrD,GAAG;AAAA,gBACH,QAAQ;AAAA,gBACR,SAAS;AAAA,kBACL,aAAa;AAAA,kBACb;AAAA,kBACA,GAAG;AAAA,gBACP;AAAA,cACJ,CAAC;AACD,kBAAI,CAAC,UAAU,GAAG,eAAe,UAAU,MAAM;AAG7C,mBAAG,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,cACjC;AAAA,YACJ;AACA,gBAAI;AACA,4BAAc,EAAE,GAAG,KAAK,QAAQ,eAAe,GAAG,SAAS;AAAA,YAC/D,SAAS,KAAK;AACV,wBAAU,EAAE,OAAO,kBAAmB,IAAc,OAAO,GAAG,CAAC;AAAA,YACnE;AAAA,UACJ,WAAW,gBAAgB,mBAAmB,IAAI,WAAW;AAGzD,kBAAM,UAAU,gBAAgB,IAAI,IAAI,SAAS;AACjD,gBAAI,SAAS;AACT,2BAAa,QAAQ,OAAO;AAC5B,8BAAgB,OAAO,IAAI,SAAS;AACpC,sBAAQ,QAAQ,IAAI,OAAO;AAAA,YAC/B;AAAA,UACJ;AAAA,QACJ,OAAO;AAEH,+BAAqB,IAAI,UAAU,GAAG;AAAA,QAC1C;AAAA,MACJ;AAAA,IACJ,QAAQ;AAAA,IAER;AAAA,EACJ,CAAC;AAED,QAAM,UAAU,CAAC,UAA6B;AAC1C,oBAAgB,OAAO,MAAM;AAE7B,eAAW,CAAC,OAAO,GAAG,KAAK,iBAAiB;AACxC,UAAI,IAAI,eAAe,QAAQ;AAC3B,qBAAa,IAAI,OAAO;AACxB,YAAI,OAAO,IAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,EAAE,CAAC;AAChD,wBAAgB,OAAO,KAAK;AAAA,MAChC;AAAA,IACJ;AAIA,wBAAoB,MAAM;AAC1B,WAAO,KAAK,WAAW,2BAA2B,KAAK,MAAM,MAAM,EAAE;AAAA,EACzE;AAEA,KAAG,GAAG,SAAS,MAAM,QAAQ,OAAO,CAAC;AACrC,KAAG,GAAG,SAAS,MAAM,QAAQ,OAAO,CAAC;AACzC;AAGO,SAAS,eACZ,aACA,SACA,aAAa,KACT;AACJ,MAAI,kBAAmB;AACvB,sBAAoB,YAAY,YAAY;AACxC,UAAM,WAAW,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAK,WAAW,CAAC;AAChF,UAAM,MAAmB;AAAA,MACrB,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC;AACA,oBAAgB,GAAG;AAAA,EACvB,GAAG,UAAU;AACb,SAAO,MAAM,WAAW,+BAA+B,KAAK,MAAM,aAAa,GAAI,CAAC,IAAI;AAC5F;AAGO,SAAS,gBAAsB;AAClC,MAAI,mBAAmB;AACnB,kBAAc,iBAAiB;AAC/B,wBAAoB;AACpB,WAAO,MAAM,WAAW,4BAA4B;AAAA,EACxD;AACJ;AAGO,SAAS,wBAAgC;AAC5C,MAAI,QAAQ;AACZ,aAAW,CAAC,EAAE,EAAE,KAAK,iBAAiB;AAClC,QAAI,GAAG,eAAe,UAAU,KAAM;AAAA,EAC1C;AACA,SAAO;AACX;AAGO,SAAS,eAAe,QAAsB;AACjD,QAAM,KAAK,gBAAgB,IAAI,MAAM;AACrC,MAAI,IAAI;AACJ,OAAG,MAAM;AACT,oBAAgB,OAAO,MAAM;AAE7B,eAAW,CAAC,OAAO,GAAG,KAAK,iBAAiB;AACxC,UAAI,IAAI,eAAe,QAAQ;AAC3B,qBAAa,IAAI,OAAO;AACxB,YAAI,OAAO,IAAI,MAAM,sBAAsB,MAAM,EAAE,CAAC;AACpD,wBAAgB,OAAO,KAAK;AAAA,MAChC;AAAA,IACJ;AACA,WAAO,KAAK,WAAW,kCAAkC,MAAM,EAAE;AAAA,EACrE;AACJ;AAGO,SAAS,gBAAsB;AAClC,gBAAc;AACd,qBAAmB;AACnB,aAAW,CAAC,QAAQ,EAAE,KAAK,iBAAiB;AACxC,OAAG,MAAM;AACT,oBAAgB,OAAO,MAAM;AAAA,EACjC;AACA,aAAW,CAAC,IAAI,GAAG,KAAK,iBAAiB;AACrC,iBAAa,IAAI,OAAO;AACxB,QAAI,OAAO,IAAI,MAAM,oBAAoB,CAAC;AAC1C,oBAAgB,OAAO,EAAE;AAAA,EAC7B;AACA,iBAAe,MAAM;AACzB;","names":["localNodeId"]}
@@ -7,6 +7,7 @@ import logger from "../utils/logger.js";
7
7
  import { fetchWithRetry } from "../utils/helpers.js";
8
8
  import { resolveApiKey } from "./authResolver.js";
9
9
  import { v4 as uuid } from "uuid";
10
+ import { clampMaxTokens } from "./modelCapabilities.js";
10
11
  const COMPONENT = "Anthropic";
11
12
  class AnthropicProvider extends LLMProvider {
12
13
  name = "anthropic";
@@ -29,7 +30,7 @@ class AnthropicProvider extends LLMProvider {
29
30
  const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
30
31
  const body = {
31
32
  model: model.replace("anthropic/", ""),
32
- max_tokens: options.maxTokens || 8192,
33
+ max_tokens: clampMaxTokens(model, options.maxTokens),
33
34
  messages: nonSystemMessages.map((m) => ({
34
35
  role: m.role === "tool" ? "user" : m.role,
35
36
  content: m.role === "tool" ? [{ type: "tool_result", tool_use_id: m.toolCallId, content: m.content }] : m.content
@@ -134,7 +135,7 @@ class AnthropicProvider extends LLMProvider {
134
135
  const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
135
136
  const body = {
136
137
  model: model.replace("anthropic/", ""),
137
- max_tokens: options.maxTokens || 8192,
138
+ max_tokens: clampMaxTokens(model, options.maxTokens),
138
139
  stream: true,
139
140
  messages: nonSystemMessages.map((m) => ({
140
141
  role: m.role === "tool" ? "user" : m.role,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/anthropic.ts"],"sourcesContent":["/**\n * TITAN — Anthropic/Claude Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\n\nconst COMPONENT = 'Anthropic';\n\nexport class AnthropicProvider extends LLMProvider {\n readonly name = 'anthropic';\n readonly displayName = 'Anthropic (Claude)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.anthropic;\n return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Anthropic API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: options.maxTokens || 8192,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n // TITAN pattern: prompt cache splitting\n // Place cache_control breakpoint on system prompt to cache it across turns.\n // This reduces input costs by ~75% for subsequent messages in the same session.\n body.system = [\n {\n type: 'text',\n text: systemMessage.content,\n cache_control: { type: 'ephemeral' },\n },\n ];\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n // Force at least one tool call on first round when task requires it.\n // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.\n if (options.forceToolUse && !options.thinking) {\n body.tool_choice = { type: 'any' };\n }\n }\n\n if (options.temperature !== undefined) {\n body.temperature = options.temperature;\n }\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const content = data.content as Array<Record<string, unknown>> | undefined;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (!content || !Array.isArray(content)) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n for (const block of content) {\n if (block.type === 'text') {\n textContent += block.text as string;\n } else if (block.type === 'tool_use') {\n toolCalls.push({\n id: block.id as string,\n type: 'function',\n function: {\n name: block.name as string,\n arguments: JSON.stringify(block.input),\n },\n });\n }\n }\n\n const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.input_tokens,\n completionTokens: usage.output_tokens,\n totalTokens: usage.input_tokens + usage.output_tokens,\n }\n : undefined,\n finishReason: (() => {\n const sr = data.stop_reason as string | undefined;\n if (sr === 'max_tokens') return 'length';\n if (sr === 'tool_use') return 'tool_calls';\n return toolCalls.length > 0 ? 'tool_calls' : 'stop';\n })(),\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: options.maxTokens || 8192,\n stream: true,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];\n }\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n }\n if (options.temperature !== undefined) body.temperature = options.temperature;\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `Anthropic API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentToolId = '';\n let currentToolName = '';\n let toolArgsBuffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]' || !json) continue;\n\n try {\n const event = JSON.parse(json);\n if (event.type === 'content_block_delta') {\n const delta = event.delta;\n if (delta.type === 'text_delta' && delta.text) {\n yield { type: 'text', content: delta.text };\n } else if (delta.type === 'input_json_delta' && delta.partial_json) {\n toolArgsBuffer += delta.partial_json;\n }\n } else if (event.type === 'content_block_start') {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n currentToolId = block.id;\n currentToolName = block.name;\n toolArgsBuffer = '';\n }\n } else if (event.type === 'content_block_stop') {\n if (currentToolId) {\n yield {\n type: 'tool_call',\n toolCall: {\n id: currentToolId,\n type: 'function',\n function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },\n },\n };\n currentToolId = '';\n toolArgsBuffer = '';\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n return [\n 'claude-opus-4-0',\n 'claude-sonnet-4-20250514',\n 'claude-haiku-4-20250414',\n 'claude-3-5-sonnet-20241022',\n 'claude-3-5-haiku-20241022',\n ];\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-20250414',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n }),\n });\n return response.ok || response.status === 400; // 400 = valid auth but bad request\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/anthropic.ts"],"sourcesContent":["/**\n * TITAN — Anthropic/Claude Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'Anthropic';\n\nexport class AnthropicProvider extends LLMProvider {\n readonly name = 'anthropic';\n readonly displayName = 'Anthropic (Claude)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.anthropic;\n return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Anthropic API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n // TITAN pattern: prompt cache splitting\n // Place cache_control breakpoint on system prompt to cache it across turns.\n // This reduces input costs by ~75% for subsequent messages in the same session.\n body.system = [\n {\n type: 'text',\n text: systemMessage.content,\n cache_control: { type: 'ephemeral' },\n },\n ];\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n // Force at least one tool call on first round when task requires it.\n // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.\n if (options.forceToolUse && !options.thinking) {\n body.tool_choice = { type: 'any' };\n }\n }\n\n if (options.temperature !== undefined) {\n body.temperature = options.temperature;\n }\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const content = data.content as Array<Record<string, unknown>> | undefined;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (!content || !Array.isArray(content)) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n for (const block of content) {\n if (block.type === 'text') {\n textContent += block.text as string;\n } else if (block.type === 'tool_use') {\n toolCalls.push({\n id: block.id as string,\n type: 'function',\n function: {\n name: block.name as string,\n arguments: JSON.stringify(block.input),\n },\n });\n }\n }\n\n const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.input_tokens,\n completionTokens: usage.output_tokens,\n totalTokens: usage.input_tokens + usage.output_tokens,\n }\n : undefined,\n finishReason: (() => {\n const sr = data.stop_reason as string | undefined;\n if (sr === 'max_tokens') return 'length';\n if (sr === 'tool_use') return 'tool_calls';\n return toolCalls.length > 0 ? 'tool_calls' : 'stop';\n })(),\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n stream: true,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];\n }\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n }\n if (options.temperature !== undefined) body.temperature = options.temperature;\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `Anthropic API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentToolId = '';\n let currentToolName = '';\n let toolArgsBuffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]' || !json) continue;\n\n try {\n const event = JSON.parse(json);\n if (event.type === 'content_block_delta') {\n const delta = event.delta;\n if (delta.type === 'text_delta' && delta.text) {\n yield { type: 'text', content: delta.text };\n } else if (delta.type === 'input_json_delta' && delta.partial_json) {\n toolArgsBuffer += delta.partial_json;\n }\n } else if (event.type === 'content_block_start') {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n currentToolId = block.id;\n currentToolName = block.name;\n toolArgsBuffer = '';\n }\n } else if (event.type === 'content_block_stop') {\n if (currentToolId) {\n yield {\n type: 'tool_call',\n toolCall: {\n id: currentToolId,\n type: 'function',\n function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },\n },\n };\n currentToolId = '';\n toolArgsBuffer = '';\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n return [\n 'claude-opus-4-0',\n 'claude-sonnet-4-20250514',\n 'claude-haiku-4-20250414',\n 'claude-3-5-sonnet-20241022',\n 'claude-3-5-haiku-20241022',\n ];\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-20250414',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n }),\n });\n return response.ok || response.status === 400; // 400 = valid auth but bad request\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/base.ts"],"sourcesContent":["/**\n * TITAN LLM Provider — Base Interface\n * All LLM providers implement this interface for a unified API.\n */\n\n/** A single message in a conversation */\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n name?: string;\n toolCallId?: string;\n toolCalls?: ToolCall[];\n}\n\n/** A tool call requested by the LLM */\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n /**\n * v4.13: Gemini 3 thinking models (via Ollama's Gemini proxy) emit a\n * `thought_signature` on each function_call part. It MUST be carried\n * through to the next turn or Gemini rejects the request with:\n * \"Function call is missing a thought_signature in functionCall parts\"\n * Other providers ignore this field.\n */\n thoughtSignature?: string;\n}\n\n/** A tool definition for the LLM */\nexport interface ToolDefinition {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n };\n}\n\n/** Options for a chat completion request */\nexport interface ChatOptions {\n model?: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n maxTokens?: number;\n temperature?: number;\n stream?: boolean;\n thinking?: boolean;\n thinkingLevel?: 'off' | 'low' | 'medium' | 'high';\n /** Force the model to call a tool on this turn (tool_choice: required/any).\n * Only set to true on the first round when the task clearly requires tool use.\n * Subsequent rounds always use auto (model decides). */\n forceToolUse?: boolean;\n /** Disable all fallback/failover behavior — fail the request if the resolved\n * target model/provider cannot serve it. Used by the empirical model probe,\n * which must hit the exact target model or report a clean failure. Without\n * this, a silent fallback would poison the capabilities registry with data\n * from whichever model happened to answer. */\n noFallback?: boolean;\n /** Ollama-native structured output. Pass a JSON schema to constrain the\n * model's output to match it, or the string 'json' for loose JSON mode.\n * Only the Ollama provider honours this today — other providers ignore it. */\n format?: Record<string, unknown> | 'json';\n /**\n * Provider-specific options bag. Keeps ChatOptions slim while letting\n * individual providers accept flags without bloating the shared type.\n * Each provider documents which keys it reads.\n *\n * Known keys today:\n * - `allowClaudeCode: boolean` — required true for ClaudeCodeProvider\n * to accept a call. All autonomous paths (autopilot, goal driver,\n * specialists, self-mod) leave it unset; Claude Code rejects\n * anything without it. Only user-initiated UI/API chat should set\n * it, after the user explicitly picks a claude-code model.\n */\n providerOptions?: Record<string, unknown>;\n\n /**\n * @deprecated v4.12 — use `providerOptions.allowClaudeCode` instead.\n * Read as a fallback for one release cycle; will be removed in v5.0.\n */\n allowClaudeCode?: boolean;\n}\n\n/**\n * Read the Claude Code opt-in flag from either the new providerOptions\n * bag (preferred) or the deprecated top-level allowClaudeCode field.\n */\nexport function isClaudeCodeAllowed(options: ChatOptions): boolean {\n const po = options.providerOptions;\n if (po && typeof po === 'object' && po.allowClaudeCode === true) return true;\n return options.allowClaudeCode === true;\n}\n\n/** Response from a chat completion */\nexport interface ChatResponse {\n id: string;\n content: string;\n toolCalls?: ToolCall[];\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n model: string;\n}\n\n/**\n * Streaming chunk from a chat completion.\n *\n * Discriminated union keyed on `type` (v4.12). Consumers switch on `type`\n * and TypeScript narrows the shape — no more optional-everything objects\n * where you have to remember which fields exist for which variant.\n */\nexport type ChatStreamChunk =\n | { type: 'text'; content: string }\n | { type: 'tool_call'; toolCall: ToolCall }\n | { type: 'done' }\n | { type: 'error'; error: string }\n | {\n type: 'failover';\n /** The provider that the request fell over to. */\n content?: string;\n /** The original provider that failed before failover. */\n originalProvider: string;\n /** The original model that failed before failover. */\n originalModel: string;\n /** The error message from the original provider. */\n error?: string;\n };\n\n/** Abstract LLM Provider interface */\nexport abstract class LLMProvider {\n abstract readonly name: string;\n abstract readonly displayName: string;\n\n /** Send a chat completion request */\n abstract chat(options: ChatOptions): Promise<ChatResponse>;\n\n /** Send a streaming chat completion request */\n abstract chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk>;\n\n /** List available models */\n abstract listModels(): Promise<string[]>;\n\n /** Check if the provider is configured and reachable */\n abstract healthCheck(): Promise<boolean>;\n\n /** Get the provider identifier from a model string like \"anthropic/claude-3\" */\n static parseModelId(modelId: string): { provider: string; model: string } {\n // E3: Guard against empty/whitespace model IDs\n if (!modelId || !modelId.trim()) {\n return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };\n }\n const parts = modelId.split('/');\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return { provider: parts[0], model: parts.slice(1).join('/') };\n }\n return { provider: 'anthropic', model: modelId };\n }\n}\n"],"mappings":";AA2FO,SAAS,oBAAoB,SAA+B;AAC/D,QAAM,KAAK,QAAQ;AACnB,MAAI,MAAM,OAAO,OAAO,YAAY,GAAG,oBAAoB,KAAM,QAAO;AACxE,SAAO,QAAQ,oBAAoB;AACvC;AAyCO,MAAe,YAAY;AAAA;AAAA,EAiB9B,OAAO,aAAa,SAAsD;AAEtE,QAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,GAAG;AAC7B,aAAO,EAAE,UAAU,aAAa,OAAO,2BAA2B;AAAA,IACtE;AACA,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AAC3C,aAAO,EAAE,UAAU,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE;AAAA,IACjE;AACA,WAAO,EAAE,UAAU,aAAa,OAAO,QAAQ;AAAA,EACnD;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/providers/base.ts"],"sourcesContent":["/**\n * TITAN LLM Provider — Base Interface\n * All LLM providers implement this interface for a unified API.\n */\n\n/** A single message in a conversation */\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n name?: string;\n toolCallId?: string;\n toolCalls?: ToolCall[];\n}\n\n/** A tool call requested by the LLM */\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n /**\n * v4.13: Gemini 3 thinking models (via Ollama's Gemini proxy) emit a\n * `thought_signature` on each function_call part. It MUST be carried\n * through to the next turn or Gemini rejects the request with:\n * \"Function call is missing a thought_signature in functionCall parts\"\n * Other providers ignore this field.\n */\n thoughtSignature?: string;\n}\n\n/** A tool definition for the LLM */\nexport interface ToolDefinition {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n };\n}\n\n/** Options for a chat completion request */\nexport interface ChatOptions {\n model?: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n maxTokens?: number;\n temperature?: number;\n stream?: boolean;\n thinking?: boolean;\n thinkingLevel?: 'off' | 'low' | 'medium' | 'high';\n /** Force the model to call a tool on this turn (tool_choice: required/any).\n * Only set to true on the first round when the task clearly requires tool use.\n * Subsequent rounds always use auto (model decides). */\n forceToolUse?: boolean;\n /** Disable all fallback/failover behavior — fail the request if the resolved\n * target model/provider cannot serve it. Used by the empirical model probe,\n * which must hit the exact target model or report a clean failure. Without\n * this, a silent fallback would poison the capabilities registry with data\n * from whichever model happened to answer. */\n noFallback?: boolean;\n /** Ollama-native structured output. Pass a JSON schema to constrain the\n * model's output to match it, or the string 'json' for loose JSON mode.\n * Only the Ollama provider honours this today — other providers ignore it. */\n format?: Record<string, unknown> | 'json';\n /**\n * Provider-specific options bag. Keeps ChatOptions slim while letting\n * individual providers accept flags without bloating the shared type.\n * Each provider documents which keys it reads.\n *\n * Known keys today:\n * - `allowClaudeCode: boolean` — required true for ClaudeCodeProvider\n * to accept a call. All autonomous paths (autopilot, goal driver,\n * specialists, self-mod) leave it unset; Claude Code rejects\n * anything without it. Only user-initiated UI/API chat should set\n * it, after the user explicitly picks a claude-code model.\n */\n providerOptions?: Record<string, unknown>;\n\n /**\n * @deprecated v4.12 — use `providerOptions.allowClaudeCode` instead.\n * Read as a fallback for one release cycle; will be removed in v5.0.\n */\n allowClaudeCode?: boolean;\n}\n\n/**\n * Read the Claude Code opt-in flag from either the new providerOptions\n * bag (preferred) or the deprecated top-level allowClaudeCode field.\n */\nexport function isClaudeCodeAllowed(options: ChatOptions): boolean {\n const po = options.providerOptions;\n if (po && typeof po === 'object' && po.allowClaudeCode === true) return true;\n return options.allowClaudeCode === true;\n}\n\n/** Response from a chat completion */\nexport interface ChatResponse {\n id: string;\n content: string;\n toolCalls?: ToolCall[];\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n model: string;\n}\n\n/**\n * Streaming chunk from a chat completion.\n *\n * Discriminated union keyed on `type` (v4.12). Consumers switch on `type`\n * and TypeScript narrows the shape — no more optional-everything objects\n * where you have to remember which fields exist for which variant.\n */\nexport type ChatStreamChunk =\n | { type: 'text'; content: string }\n | { type: 'tool_call'; toolCall: ToolCall }\n | { type: 'done' }\n | { type: 'error'; error: string }\n | {\n /**\n * Out-of-band retry status — emitted while the router is retrying a\n * transient failure on the same provider. Stream consumers MUST NOT\n * forward `content` to user-visible output; it's a signal for UI\n * status indicators (spinners, toasts) and structured logs only.\n *\n * Pre-fix (v5.4.x): the router yielded a `text` chunk like\n * \"[Retrying request (1/4) due to rate_limit...]\" which leaked into\n * model responses. The dedicated chunk type isolates that signal.\n */\n type: 'retry';\n /** Which retry this is (1-based). */\n attempt: number;\n /** Configured maximum number of retries before failover. */\n maxRetries: number;\n /** Classified failure reason (e.g. \"rate_limit\", \"server_error\"). */\n reason: string;\n /** Provider being retried. */\n provider: string;\n /** Model being retried. */\n model: string;\n /** Backoff delay in ms before the next attempt. */\n delayMs: number;\n }\n | {\n type: 'failover';\n /** The provider that the request fell over to. */\n content?: string;\n /** The original provider that failed before failover. */\n originalProvider: string;\n /** The original model that failed before failover. */\n originalModel: string;\n /** The error message from the original provider. */\n error?: string;\n };\n\n/** Abstract LLM Provider interface */\nexport abstract class LLMProvider {\n abstract readonly name: string;\n abstract readonly displayName: string;\n\n /** Send a chat completion request */\n abstract chat(options: ChatOptions): Promise<ChatResponse>;\n\n /** Send a streaming chat completion request */\n abstract chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk>;\n\n /** List available models */\n abstract listModels(): Promise<string[]>;\n\n /** Check if the provider is configured and reachable */\n abstract healthCheck(): Promise<boolean>;\n\n /** Get the provider identifier from a model string like \"anthropic/claude-3\" */\n static parseModelId(modelId: string): { provider: string; model: string } {\n // E3: Guard against empty/whitespace model IDs\n if (!modelId || !modelId.trim()) {\n return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };\n }\n const parts = modelId.split('/');\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return { provider: parts[0], model: parts.slice(1).join('/') };\n }\n return { provider: 'anthropic', model: modelId };\n }\n}\n"],"mappings":";AA2FO,SAAS,oBAAoB,SAA+B;AAC/D,QAAM,KAAK,QAAQ;AACnB,MAAI,MAAM,OAAO,OAAO,YAAY,GAAG,oBAAoB,KAAM,QAAO;AACxE,SAAO,QAAQ,oBAAoB;AACvC;AAkEO,MAAe,YAAY;AAAA;AAAA,EAiB9B,OAAO,aAAa,SAAsD;AAEtE,QAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,GAAG;AAC7B,aAAO,EAAE,UAAU,aAAa,OAAO,2BAA2B;AAAA,IACtE;AACA,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AAC3C,aAAO,EAAE,UAAU,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE;AAAA,IACjE;AACA,WAAO,EAAE,UAAU,aAAa,OAAO,QAAQ;AAAA,EACnD;AACJ;","names":[]}