@lobu/gateway 3.0.7 → 3.0.9

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 (90) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +1 -0
  3. package/dist/api/platform.js.map +1 -1
  4. package/dist/auth/bedrock/models.d.ts +13 -0
  5. package/dist/auth/bedrock/models.d.ts.map +1 -0
  6. package/dist/auth/bedrock/models.js +75 -0
  7. package/dist/auth/bedrock/models.js.map +1 -0
  8. package/dist/auth/bedrock/provider-module.d.ts +17 -0
  9. package/dist/auth/bedrock/provider-module.d.ts.map +1 -0
  10. package/dist/auth/bedrock/provider-module.js +80 -0
  11. package/dist/auth/bedrock/provider-module.js.map +1 -0
  12. package/dist/auth/external/device-code-client.d.ts +2 -0
  13. package/dist/auth/external/device-code-client.d.ts.map +1 -1
  14. package/dist/auth/external/device-code-client.js +12 -4
  15. package/dist/auth/external/device-code-client.js.map +1 -1
  16. package/dist/auth/mcp/config-service.d.ts +3 -4
  17. package/dist/auth/mcp/config-service.d.ts.map +1 -1
  18. package/dist/auth/mcp/config-service.js +40 -12
  19. package/dist/auth/mcp/config-service.js.map +1 -1
  20. package/dist/auth/mcp/proxy.d.ts +1 -3
  21. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  22. package/dist/auth/mcp/proxy.js.map +1 -1
  23. package/dist/cli/gateway.d.ts.map +1 -1
  24. package/dist/cli/gateway.js +12 -5
  25. package/dist/cli/gateway.js.map +1 -1
  26. package/dist/cli/index.js +2 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  29. package/dist/connections/interaction-bridge.js +57 -15
  30. package/dist/connections/interaction-bridge.js.map +1 -1
  31. package/dist/connections/message-handler-bridge.d.ts.map +1 -1
  32. package/dist/connections/message-handler-bridge.js +44 -26
  33. package/dist/connections/message-handler-bridge.js.map +1 -1
  34. package/dist/gateway/index.d.ts.map +1 -1
  35. package/dist/gateway/index.js +1 -3
  36. package/dist/gateway/index.js.map +1 -1
  37. package/dist/orchestration/base-deployment-manager.js +7 -7
  38. package/dist/orchestration/base-deployment-manager.js.map +1 -1
  39. package/dist/platform/unified-thread-consumer.d.ts.map +1 -1
  40. package/dist/platform/unified-thread-consumer.js +38 -34
  41. package/dist/platform/unified-thread-consumer.js.map +1 -1
  42. package/dist/routes/internal/device-auth.d.ts +7 -0
  43. package/dist/routes/internal/device-auth.d.ts.map +1 -1
  44. package/dist/routes/internal/device-auth.js +101 -48
  45. package/dist/routes/internal/device-auth.js.map +1 -1
  46. package/dist/routes/public/cli-auth.d.ts.map +1 -1
  47. package/dist/routes/public/cli-auth.js +10 -0
  48. package/dist/routes/public/cli-auth.js.map +1 -1
  49. package/dist/services/bedrock-anthropic-service.d.ts +87 -0
  50. package/dist/services/bedrock-anthropic-service.d.ts.map +1 -0
  51. package/dist/services/bedrock-anthropic-service.js +453 -0
  52. package/dist/services/bedrock-anthropic-service.js.map +1 -0
  53. package/dist/services/bedrock-model-catalog.d.ts +28 -0
  54. package/dist/services/bedrock-model-catalog.d.ts.map +1 -0
  55. package/dist/services/bedrock-model-catalog.js +160 -0
  56. package/dist/services/bedrock-model-catalog.js.map +1 -0
  57. package/dist/services/bedrock-openai-service.d.ts +119 -0
  58. package/dist/services/bedrock-openai-service.d.ts.map +1 -0
  59. package/dist/services/bedrock-openai-service.js +412 -0
  60. package/dist/services/bedrock-openai-service.js.map +1 -0
  61. package/dist/services/core-services.d.ts +3 -0
  62. package/dist/services/core-services.d.ts.map +1 -1
  63. package/dist/services/core-services.js +13 -0
  64. package/dist/services/core-services.js.map +1 -1
  65. package/dist/services/system-config-resolver.d.ts.map +1 -1
  66. package/dist/services/system-config-resolver.js +0 -2
  67. package/dist/services/system-config-resolver.js.map +1 -1
  68. package/package.json +12 -10
  69. package/src/__tests__/bedrock-model-catalog.test.ts +40 -0
  70. package/src/__tests__/bedrock-openai-service.test.ts +157 -0
  71. package/src/__tests__/bedrock-provider-module.test.ts +56 -0
  72. package/src/__tests__/mcp-config-service.test.ts +1 -1
  73. package/src/__tests__/mcp-proxy.test.ts +1 -3
  74. package/src/auth/bedrock/provider-module.ts +110 -0
  75. package/src/auth/external/device-code-client.ts +14 -4
  76. package/src/auth/mcp/config-service.ts +49 -21
  77. package/src/auth/mcp/proxy.ts +1 -3
  78. package/src/cli/gateway.ts +8 -0
  79. package/src/cli/index.ts +2 -2
  80. package/src/connections/message-handler-bridge.ts +76 -51
  81. package/src/gateway/index.ts +1 -3
  82. package/src/orchestration/base-deployment-manager.ts +7 -7
  83. package/src/platform/unified-thread-consumer.ts +49 -42
  84. package/src/routes/internal/device-auth.ts +137 -51
  85. package/src/routes/public/cli-auth.ts +13 -0
  86. package/src/services/bedrock-model-catalog.ts +217 -0
  87. package/src/services/bedrock-openai-service.ts +658 -0
  88. package/src/services/core-services.ts +19 -0
  89. package/src/services/system-config-resolver.ts +0 -1
  90. package/tsconfig.tsbuildinfo +1 -0
@@ -10,6 +10,8 @@ export interface DeviceCodeClientConfig {
10
10
  tokenUrl: string;
11
11
  deviceAuthorizationUrl: string;
12
12
  scope: string;
13
+ /** RFC 8707 resource indicator included in token requests. */
14
+ resource?: string;
13
15
  tokenEndpointAuthMethod?:
14
16
  | "none"
15
17
  | "client_secret_post"
@@ -68,10 +70,14 @@ export class GenericDeviceCodeClient extends BaseOAuth2Client {
68
70
  }
69
71
 
70
72
  async requestDeviceCode(): Promise<DeviceAuthorizationStartResult> {
71
- const { headers, body } = this.buildAuthenticatedFormBody({
73
+ const params: Record<string, string> = {
72
74
  client_id: this.config.clientId,
73
75
  scope: this.config.scope,
74
- });
76
+ };
77
+ if (this.config.resource) {
78
+ params.resource = this.config.resource;
79
+ }
80
+ const { headers, body } = this.buildAuthenticatedFormBody(params);
75
81
 
76
82
  const response = await fetch(this.config.deviceAuthorizationUrl, {
77
83
  method: "POST",
@@ -103,11 +109,15 @@ export class GenericDeviceCodeClient extends BaseOAuth2Client {
103
109
  deviceAuthId: string,
104
110
  intervalSeconds?: number
105
111
  ): Promise<DeviceAuthorizationPollResult> {
106
- const { headers, body } = this.buildAuthenticatedFormBody({
112
+ const params: Record<string, string> = {
107
113
  grant_type: DEVICE_CODE_GRANT_TYPE,
108
114
  device_code: deviceAuthId,
109
115
  client_id: this.config.clientId,
110
- });
116
+ };
117
+ if (this.config.resource) {
118
+ params.resource = this.config.resource;
119
+ }
120
+ const { headers, body } = this.buildAuthenticatedFormBody(params);
111
121
 
112
122
  const response = await fetch(this.config.tokenUrl, {
113
123
  method: "POST",
@@ -1,4 +1,8 @@
1
- import { createLogger, verifyWorkerToken } from "@lobu/core";
1
+ import {
2
+ type McpOAuthConfig,
3
+ createLogger,
4
+ verifyWorkerToken,
5
+ } from "@lobu/core";
2
6
  import type { SystemConfigResolver } from "../../services/system-config-resolver";
3
7
  import type { AgentSettingsStore } from "../settings/agent-settings-store";
4
8
 
@@ -10,14 +14,12 @@ interface McpInput {
10
14
  description: string;
11
15
  }
12
16
 
13
- interface HttpMcpServerConfig {
17
+ export interface HttpMcpServerConfig {
14
18
  id: string;
15
19
  upstreamUrl: string;
16
- oauth?: unknown;
20
+ oauth?: McpOAuthConfig;
17
21
  inputs?: McpInput[];
18
22
  headers?: Record<string, string>;
19
- loginUrl?: string;
20
- resource?: string;
21
23
  }
22
24
 
23
25
  interface WorkerMcpConfig {
@@ -182,9 +184,7 @@ export class McpConfigService {
182
184
  const statuses: McpStatus[] = [];
183
185
 
184
186
  for (const [id, httpServer] of httpServers) {
185
- const hasOAuth = !!httpServer.oauth;
186
- const hasLoginUrl = !!httpServer.loginUrl;
187
- const requiresAuth = hasOAuth || hasLoginUrl;
187
+ const requiresAuth = !!httpServer.oauth;
188
188
  const requiresInput = !!(
189
189
  httpServer.inputs && httpServer.inputs.length > 0
190
190
  );
@@ -280,6 +280,45 @@ export class McpConfigService {
280
280
  }
281
281
  }
282
282
 
283
+ /**
284
+ * Parse and validate an oauth config from raw MCP server config.
285
+ * Handles backward compat: migrates top-level `resource` into `oauth.resource`,
286
+ * and treats `loginUrl` presence as `oauth: {}` (requiresAuth flag).
287
+ */
288
+ function parseOAuthConfig(raw: any): McpOAuthConfig | undefined {
289
+ const hasLoginUrl = typeof raw.loginUrl === "string";
290
+ const hasOAuth = raw.oauth && typeof raw.oauth === "object";
291
+
292
+ if (!hasOAuth && !hasLoginUrl && typeof raw.resource !== "string") {
293
+ return undefined;
294
+ }
295
+
296
+ const config: McpOAuthConfig = {};
297
+
298
+ if (hasOAuth) {
299
+ const obj = raw.oauth;
300
+ if (typeof obj.authUrl === "string") config.authUrl = obj.authUrl;
301
+ if (typeof obj.tokenUrl === "string") config.tokenUrl = obj.tokenUrl;
302
+ if (typeof obj.clientId === "string") config.clientId = obj.clientId;
303
+ if (typeof obj.clientSecret === "string")
304
+ config.clientSecret = obj.clientSecret;
305
+ if (Array.isArray(obj.scopes))
306
+ config.scopes = obj.scopes.filter((s: unknown) => typeof s === "string");
307
+ if (typeof obj.deviceAuthorizationUrl === "string")
308
+ config.deviceAuthorizationUrl = obj.deviceAuthorizationUrl;
309
+ if (typeof obj.registrationUrl === "string")
310
+ config.registrationUrl = obj.registrationUrl;
311
+ if (typeof obj.resource === "string") config.resource = obj.resource;
312
+ }
313
+
314
+ // Migrate top-level resource into oauth.resource (backward compat)
315
+ if (typeof raw.resource === "string" && !config.resource) {
316
+ config.resource = raw.resource;
317
+ }
318
+
319
+ return config;
320
+ }
321
+
283
322
  function normalizeConfig(config: { mcpServers: Record<string, any> }) {
284
323
  const rawServers: Record<string, any> = {};
285
324
  const httpServers = new Map<string, HttpMcpServerConfig>();
@@ -296,10 +335,7 @@ function normalizeConfig(config: { mcpServers: Record<string, any> }) {
296
335
  httpServers.set(id, {
297
336
  id,
298
337
  upstreamUrl: cloned.url,
299
- oauth:
300
- cloned.oauth && typeof cloned.oauth === "object"
301
- ? cloned.oauth
302
- : undefined,
338
+ oauth: parseOAuthConfig(cloned),
303
339
  inputs: Array.isArray(cloned.inputs)
304
340
  ? cloned.inputs.filter(
305
341
  (input: any) =>
@@ -312,10 +348,6 @@ function normalizeConfig(config: { mcpServers: Record<string, any> }) {
312
348
  cloned.headers && typeof cloned.headers === "object"
313
349
  ? cloned.headers
314
350
  : undefined,
315
- loginUrl:
316
- typeof cloned.loginUrl === "string" ? cloned.loginUrl : undefined,
317
- resource:
318
- typeof cloned.resource === "string" ? cloned.resource : undefined,
319
351
  });
320
352
  }
321
353
  }
@@ -343,10 +375,7 @@ function toHttpServerConfig(
343
375
  return {
344
376
  id,
345
377
  upstreamUrl: cloned.url,
346
- oauth:
347
- cloned.oauth && typeof cloned.oauth === "object"
348
- ? cloned.oauth
349
- : undefined,
378
+ oauth: parseOAuthConfig(cloned),
350
379
  inputs: Array.isArray(cloned.inputs)
351
380
  ? cloned.inputs.filter(
352
381
  (input: any) =>
@@ -357,7 +386,6 @@ function toHttpServerConfig(
357
386
  cloned.headers && typeof cloned.headers === "object"
358
387
  ? cloned.headers
359
388
  : undefined,
360
- loginUrl: typeof cloned.loginUrl === "string" ? cloned.loginUrl : undefined,
361
389
  };
362
390
  }
363
391
 
@@ -85,11 +85,9 @@ interface JsonRpcResponse {
85
85
  interface HttpMcpServerConfig {
86
86
  id: string;
87
87
  upstreamUrl: string;
88
- oauth?: unknown;
88
+ oauth?: import("@lobu/core").McpOAuthConfig;
89
89
  inputs?: unknown[];
90
90
  headers?: Record<string, string>;
91
- loginUrl?: string;
92
- resource?: string;
93
91
  }
94
92
 
95
93
  interface McpConfigSource {
@@ -136,6 +136,14 @@ export function createGatewayApp(
136
136
  logger.debug("Secret proxy enabled at :8080/api/proxy");
137
137
  }
138
138
 
139
+ if (coreServices) {
140
+ const bedrockOpenAIService = coreServices.getBedrockOpenAIService?.();
141
+ if (bedrockOpenAIService) {
142
+ app.route("/api/bedrock", bedrockOpenAIService.getApp());
143
+ logger.debug("Bedrock routes enabled at :8080/api/bedrock/*");
144
+ }
145
+ }
146
+
139
147
  // Worker Gateway routes (Hono)
140
148
  if (workerGateway) {
141
149
  app.route("/worker", workerGateway.getApp());
package/src/cli/index.ts CHANGED
@@ -34,8 +34,8 @@ async function main() {
34
34
  initTracing({
35
35
  serviceName: "lobu-gateway",
36
36
  serviceVersion: process.env.npm_package_version || "2.0.0",
37
- tempoEndpoint: process.env.TEMPO_ENDPOINT,
38
- enabled: !!process.env.TEMPO_ENDPOINT,
37
+ otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
38
+ enabled: !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
39
39
  });
40
40
 
41
41
  const config = buildGatewayConfig();
@@ -4,7 +4,12 @@
4
4
  * settings links, allowlist, audio transcription, etc.
5
5
  */
6
6
 
7
- import { createLogger, generateTraceId } from "@lobu/core";
7
+ import {
8
+ createLogger,
9
+ createRootSpan,
10
+ flushTracing,
11
+ generateTraceId,
12
+ } from "@lobu/core";
8
13
  import type Redis from "ioredis";
9
14
  import type { CommandDispatcher } from "../commands/command-dispatcher";
10
15
  import { createChatReply } from "../commands/command-reply-adapters";
@@ -290,62 +295,82 @@ class MessageHandlerBridge {
290
295
  const traceId = generateTraceId(messageId);
291
296
  const agentSettingsStore = this.services.getAgentSettingsStore();
292
297
 
293
- // Check if agent has any provider credentials before enqueuing
294
- if (!(await hasConfiguredProvider(agentId, agentSettingsStore))) {
295
- await thread.post(
296
- "No AI provider is configured yet. Provider setup is not available in the end-user chat flow yet. Ask an admin to connect a provider for the base agent."
297
- );
298
- return;
299
- }
298
+ // Create root span for distributed tracing
299
+ const { span: rootSpan, traceparent } = createRootSpan("message_received", {
300
+ "lobu.agent_id": agentId,
301
+ "lobu.message_id": messageId,
302
+ "lobu.platform": platform,
303
+ "lobu.connection_id": this.connection.id,
304
+ });
300
305
 
301
- const agentOptions = await resolveAgentOptions(
302
- agentId,
303
- {},
304
- agentSettingsStore
305
- );
306
+ try {
307
+ // Check if agent has any provider credentials before enqueuing
308
+ if (!(await hasConfiguredProvider(agentId, agentSettingsStore))) {
309
+ await thread.post(
310
+ "No AI provider is configured yet. Provider setup is not available in the end-user chat flow yet. Ask an admin to connect a provider for the base agent."
311
+ );
312
+ return;
313
+ }
306
314
 
307
- const payload = buildMessagePayload({
308
- platform,
309
- userId,
310
- botId: platform,
311
- conversationId: isGroup ? messageId : channelId,
312
- teamId: isGroup ? channelId : platform,
313
- agentId,
314
- messageId,
315
- messageText,
316
- channelId,
317
- platformMetadata: {
318
- traceId,
315
+ const agentOptions = await resolveAgentOptions(
319
316
  agentId,
320
- chatId: channelId,
321
- senderId: userId,
322
- senderUsername: message.author?.userName,
323
- senderDisplayName: message.author?.fullName,
324
- isGroup,
325
- connectionId: this.connection.id,
326
- responseChannel: channelId,
327
- responseId: messageId,
328
- responseThreadId: thread.id,
329
- conversationHistory:
330
- conversationHistory.length > 0 ? conversationHistory : undefined,
331
- ...(sessionReset && { sessionReset: true }),
332
- },
333
- agentOptions,
334
- });
317
+ {},
318
+ agentSettingsStore
319
+ );
335
320
 
336
- const queueProducer = this.services.getQueueProducer();
337
- await queueProducer.enqueueMessage(payload);
321
+ const payload = buildMessagePayload({
322
+ platform,
323
+ userId,
324
+ botId: platform,
325
+ conversationId: isGroup ? messageId : channelId,
326
+ teamId: isGroup ? channelId : platform,
327
+ agentId,
328
+ messageId,
329
+ messageText,
330
+ channelId,
331
+ platformMetadata: {
332
+ traceId,
333
+ traceparent: traceparent || undefined,
334
+ agentId,
335
+ chatId: channelId,
336
+ senderId: userId,
337
+ senderUsername: message.author?.userName,
338
+ senderDisplayName: message.author?.fullName,
339
+ isGroup,
340
+ connectionId: this.connection.id,
341
+ responseChannel: channelId,
342
+ responseId: messageId,
343
+ responseThreadId: thread.id,
344
+ conversationHistory:
345
+ conversationHistory.length > 0 ? conversationHistory : undefined,
346
+ ...(sessionReset && { sessionReset: true }),
347
+ },
348
+ agentOptions,
349
+ });
350
+
351
+ const queueProducer = this.services.getQueueProducer();
352
+ await queueProducer.enqueueMessage(payload);
338
353
 
339
- logger.info(
340
- { traceId, messageId, agentId, connectionId: this.connection.id },
341
- "Message enqueued via Chat SDK bridge"
342
- );
354
+ logger.info(
355
+ {
356
+ traceId,
357
+ traceparent,
358
+ messageId,
359
+ agentId,
360
+ connectionId: this.connection.id,
361
+ },
362
+ "Message enqueued via Chat SDK bridge"
363
+ );
343
364
 
344
- // Show typing indicator
345
- try {
346
- await thread.startTyping?.("Processing...");
347
- } catch {
348
- // best effort
365
+ // Show typing indicator
366
+ try {
367
+ await thread.startTyping?.("Processing...");
368
+ } catch {
369
+ // best effort
370
+ }
371
+ } finally {
372
+ rootSpan?.end();
373
+ void flushTracing();
349
374
  }
350
375
  }
351
376
 
@@ -662,9 +662,7 @@ export class WorkerGateway {
662
662
  if (primaryProvider) {
663
663
  result.credentialEnvVarName = primaryProvider.getCredentialEnvVarName();
664
664
  const upstream = primaryProvider.getUpstreamConfig?.();
665
- if (upstream?.slug) {
666
- result.defaultProvider = upstream.slug;
667
- }
665
+ result.defaultProvider = upstream?.slug || primaryProvider.providerId;
668
666
  }
669
667
 
670
668
  if (agentModel) {
@@ -480,15 +480,15 @@ export abstract class BaseDeploymentManager {
480
480
  envVars.TRACE_ID = traceId;
481
481
  }
482
482
 
483
- // Add Tempo endpoint for distributed tracing
484
- const tempoEndpoint = process.env.TEMPO_ENDPOINT;
485
- if (tempoEndpoint) {
486
- envVars.TEMPO_ENDPOINT = tempoEndpoint;
483
+ // Add OTLP endpoint for distributed tracing
484
+ const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
485
+ if (otlpEndpoint) {
486
+ envVars.OTEL_EXPORTER_OTLP_ENDPOINT = otlpEndpoint;
487
487
  try {
488
- const tempoUrl = new URL(tempoEndpoint);
489
- envVars.NO_PROXY = `${envVars.NO_PROXY},${tempoUrl.hostname}`;
488
+ const otlpUrl = new URL(otlpEndpoint);
489
+ envVars.NO_PROXY = `${envVars.NO_PROXY},${otlpUrl.hostname}`;
490
490
  } catch {
491
- envVars.NO_PROXY = `${envVars.NO_PROXY},lobu-tempo`;
491
+ envVars.NO_PROXY = `${envVars.NO_PROXY},tempo`;
492
492
  }
493
493
  }
494
494
 
@@ -4,7 +4,7 @@
4
4
  * via the PlatformRegistry, eliminating duplicate queue filtering logic.
5
5
  */
6
6
 
7
- import { createLogger } from "@lobu/core";
7
+ import { createChildSpan, createLogger, flushTracing } from "@lobu/core";
8
8
  import type { ChatResponseBridge } from "../connections/chat-response-bridge";
9
9
  import type {
10
10
  IMessageQueue,
@@ -77,61 +77,68 @@ export class UnifiedThreadResponseConsumer {
77
77
  return;
78
78
  }
79
79
 
80
- // Check if this response belongs to a Chat SDK connection — handle before legacy routing
81
- if (this.chatResponseBridge?.canHandle(data)) {
82
- const sessionKey = `${data.userId}:${data.originalMessageId || data.messageId}`;
83
- try {
80
+ // Create child span for response processing (linked to original trace)
81
+ const traceparent = data.platformMetadata?.traceparent as
82
+ | string
83
+ | undefined;
84
+ const span = createChildSpan("response_delivery", traceparent, {
85
+ "lobu.message_id": data.messageId,
86
+ "lobu.user_id": data.userId,
87
+ "lobu.platform": data.platform || data.teamId || "unknown",
88
+ });
89
+
90
+ try {
91
+ // Check if this response belongs to a Chat SDK connection — handle before legacy routing
92
+ if (this.chatResponseBridge?.canHandle(data)) {
93
+ const sessionKey = `${data.userId}:${data.originalMessageId || data.messageId}`;
84
94
  await this.routeToRenderer(this.chatResponseBridge, data, sessionKey);
85
- } catch (error) {
86
- logger.error("Error processing Chat SDK response:", error);
87
- throw error;
95
+ return;
88
96
  }
89
- return;
90
- }
91
97
 
92
- // Use platform field, fall back to teamId
93
- const platformName = data.platform || data.teamId;
94
- if (!platformName) {
95
- logger.warn(
96
- `Missing platform in thread response for message ${data.messageId}, skipping`
97
- );
98
- return;
99
- }
98
+ // Use platform field, fall back to teamId
99
+ const platformName = data.platform || data.teamId;
100
+ if (!platformName) {
101
+ logger.warn(
102
+ `Missing platform in thread response for message ${data.messageId}, skipping`
103
+ );
104
+ return;
105
+ }
100
106
 
101
- // Get platform adapter from registry
102
- const platform = this.platformRegistry.get(platformName);
103
- if (!platform) {
104
- logger.warn(
105
- `No platform adapter registered for: ${platformName}, skipping message ${data.messageId}`
106
- );
107
- return;
108
- }
107
+ // Get platform adapter from registry
108
+ const platform = this.platformRegistry.get(platformName);
109
+ if (!platform) {
110
+ logger.warn(
111
+ `No platform adapter registered for: ${platformName}, skipping message ${data.messageId}`
112
+ );
113
+ return;
114
+ }
109
115
 
110
- // Get renderer from platform
111
- const renderer = platform.getResponseRenderer?.();
112
- if (!renderer) {
113
- logger.warn(
114
- `Platform ${platformName} does not provide a response renderer, skipping message ${data.messageId}`
115
- );
116
- return;
117
- }
116
+ // Get renderer from platform
117
+ const renderer = platform.getResponseRenderer?.();
118
+ if (!renderer) {
119
+ logger.warn(
120
+ `Platform ${platformName} does not provide a response renderer, skipping message ${data.messageId}`
121
+ );
122
+ return;
123
+ }
118
124
 
119
- // Create session key for tracking
120
- const sessionKey = `${data.userId}:${data.originalMessageId || data.messageId}`;
125
+ // Create session key for tracking
126
+ const sessionKey = `${data.userId}:${data.originalMessageId || data.messageId}`;
121
127
 
122
- logger.info(
123
- `Processing thread response for platform=${platformName}, message=${data.messageId}, session=${sessionKey}`
124
- );
128
+ logger.info(
129
+ `Processing thread response for platform=${platformName}, message=${data.messageId}, session=${sessionKey}`
130
+ );
125
131
 
126
- try {
127
132
  await this.routeToRenderer(renderer, data, sessionKey);
128
133
  } catch (error) {
129
134
  logger.error(
130
- `Error processing thread response for ${platformName}:`,
135
+ `Error processing thread response for message ${data.messageId}:`,
131
136
  error
132
137
  );
133
- // Let queue handle retry logic
134
138
  throw error;
139
+ } finally {
140
+ span?.end();
141
+ void flushTracing();
135
142
  }
136
143
  }
137
144