@johpaz/hive-agents 0.0.37 → 0.0.38

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 (70) hide show
  1. package/README.md +16 -16
  2. package/dist/hive.js +107 -105
  3. package/dist/tool-worker.js +25 -36
  4. package/dist/ui/assets/{AgentCreateForm-tJZv9FZC.js → AgentCreateForm-0oFbN3gj.js} +1 -1
  5. package/dist/ui/assets/{AgentDetailPage-Du-mRcAX.js → AgentDetailPage-BJ4L2fNJ.js} +1 -1
  6. package/dist/ui/assets/AgentNewPage-B3n0LUck.js +1 -0
  7. package/dist/ui/{dist/assets/AgentsPage-YvSgWRiw.js → assets/AgentsPage-DGNLDXjR.js} +1 -1
  8. package/dist/ui/assets/{CanvasPage-DtMwGvxf.js → CanvasPage-CnMO1FN8.js} +1 -1
  9. package/dist/ui/assets/{ChannelsPage-BdBXWHjj.js → ChannelsPage-fbF8K4MR.js} +1 -1
  10. package/dist/ui/{dist/assets/DashboardPage-ghl1ZguH.js → assets/DashboardPage-VyXXp3U1.js} +1 -1
  11. package/dist/ui/assets/{LoginPage-CAmSI9Vy.js → LoginPage-DPj2s2Qq.js} +1 -1
  12. package/dist/ui/{dist/assets/LogsPage-DAPBHkwK.js → assets/LogsPage-B2lY9maY.js} +1 -1
  13. package/dist/ui/{dist/assets/MeetingPage-WjjGOqqU.js → assets/MeetingPage-2ky_hKiG.js} +1 -1
  14. package/dist/ui/{dist/assets/ProvidersPage-Ct6HsAi1.js → assets/ProvidersPage-CEyUM2tD.js} +1 -1
  15. package/dist/ui/{dist/assets/RecoverPage-DpW3l-yv.js → assets/RecoverPage-B-hDZUM2.js} +1 -1
  16. package/dist/ui/{dist/assets/SettingsPage-DBJ7_E6C.js → assets/SettingsPage-eO0i3g8p.js} +1 -1
  17. package/dist/ui/assets/{SetupPage-DKmLVUaj.js → SetupPage-ByYqTELb.js} +1 -1
  18. package/dist/ui/assets/WebChatPage-BuGT2AL0.js +16 -0
  19. package/dist/ui/assets/{alert-C-NE-P3s.js → alert-Bq6awLlW.js} +1 -1
  20. package/dist/ui/{dist/assets/alert-dialog-C5mzbHdP.js → assets/alert-dialog-DQvltYmf.js} +1 -1
  21. package/dist/ui/assets/{badge-ChpACfWO.js → badge-DXUDdTed.js} +1 -1
  22. package/dist/ui/assets/{dialog-QnZ0ad8O.js → dialog-bI9jImCS.js} +1 -1
  23. package/dist/ui/assets/{es-NQNoaWDx.js → es-Cg8zdT52.js} +1 -1
  24. package/dist/ui/{dist/assets/index-DMCjjdqf.js → assets/index-CQ7fn00w.js} +2 -2
  25. package/dist/ui/assets/{label-D2H1IR_J.js → label-CrH0Jj3v.js} +1 -1
  26. package/dist/ui/assets/useProviders-CnlC_qCS.js +1 -0
  27. package/dist/ui/dist/assets/{AgentCreateForm-tJZv9FZC.js → AgentCreateForm-0oFbN3gj.js} +1 -1
  28. package/dist/ui/dist/assets/{AgentDetailPage-Du-mRcAX.js → AgentDetailPage-BJ4L2fNJ.js} +1 -1
  29. package/dist/ui/dist/assets/AgentNewPage-B3n0LUck.js +1 -0
  30. package/dist/ui/{assets/AgentsPage-YvSgWRiw.js → dist/assets/AgentsPage-DGNLDXjR.js} +1 -1
  31. package/dist/ui/dist/assets/{CanvasPage-DtMwGvxf.js → CanvasPage-CnMO1FN8.js} +1 -1
  32. package/dist/ui/dist/assets/{ChannelsPage-BdBXWHjj.js → ChannelsPage-fbF8K4MR.js} +1 -1
  33. package/dist/ui/{assets/DashboardPage-ghl1ZguH.js → dist/assets/DashboardPage-VyXXp3U1.js} +1 -1
  34. package/dist/ui/dist/assets/{LoginPage-CAmSI9Vy.js → LoginPage-DPj2s2Qq.js} +1 -1
  35. package/dist/ui/{assets/LogsPage-DAPBHkwK.js → dist/assets/LogsPage-B2lY9maY.js} +1 -1
  36. package/dist/ui/{assets/MeetingPage-WjjGOqqU.js → dist/assets/MeetingPage-2ky_hKiG.js} +1 -1
  37. package/dist/ui/{assets/ProvidersPage-Ct6HsAi1.js → dist/assets/ProvidersPage-CEyUM2tD.js} +1 -1
  38. package/dist/ui/{assets/RecoverPage-DpW3l-yv.js → dist/assets/RecoverPage-B-hDZUM2.js} +1 -1
  39. package/dist/ui/{assets/SettingsPage-DBJ7_E6C.js → dist/assets/SettingsPage-eO0i3g8p.js} +1 -1
  40. package/dist/ui/dist/assets/{SetupPage-DKmLVUaj.js → SetupPage-ByYqTELb.js} +1 -1
  41. package/dist/ui/dist/assets/WebChatPage-BuGT2AL0.js +16 -0
  42. package/dist/ui/dist/assets/{alert-C-NE-P3s.js → alert-Bq6awLlW.js} +1 -1
  43. package/dist/ui/{assets/alert-dialog-C5mzbHdP.js → dist/assets/alert-dialog-DQvltYmf.js} +1 -1
  44. package/dist/ui/dist/assets/{badge-ChpACfWO.js → badge-DXUDdTed.js} +1 -1
  45. package/dist/ui/dist/assets/{dialog-QnZ0ad8O.js → dialog-bI9jImCS.js} +1 -1
  46. package/dist/ui/dist/assets/{es-NQNoaWDx.js → es-Cg8zdT52.js} +1 -1
  47. package/dist/ui/{assets/index-DMCjjdqf.js → dist/assets/index-CQ7fn00w.js} +2 -2
  48. package/dist/ui/dist/assets/{label-D2H1IR_J.js → label-CrH0Jj3v.js} +1 -1
  49. package/dist/ui/dist/assets/useProviders-CnlC_qCS.js +1 -0
  50. package/dist/ui/dist/index.html +1 -1
  51. package/dist/ui/index.html +1 -1
  52. package/package.json +1 -1
  53. package/packages/cli/src/commands/gateway.ts +1 -1
  54. package/packages/cli/src/commands/onboard.ts +1 -1
  55. package/packages/core/src/agent/agent-loop.ts +4 -14
  56. package/packages/core/src/agent/context-compiler.ts +1 -1
  57. package/packages/core/src/agent/conversation-store.ts +4 -5
  58. package/packages/core/src/agent/providers/index.ts +3 -4
  59. package/packages/core/src/agent/tool-selector.ts +3 -4
  60. package/packages/core/src/gateway/resolver.ts +5 -1
  61. package/packages/core/src/gateway/routes/chat.ts +16 -16
  62. package/packages/core/src/gateway/server.ts +44 -45
  63. package/packages/core/src/storage/seed.ts +39 -33
  64. package/packages/core/src/tool-runtime/index.ts +20 -0
  65. package/dist/ui/assets/AgentNewPage-DIFYd_Ys.js +0 -1
  66. package/dist/ui/assets/WebChatPage-CVRcKept.js +0 -16
  67. package/dist/ui/assets/useProviders-C6_QHsEi.js +0 -1
  68. package/dist/ui/dist/assets/AgentNewPage-DIFYd_Ys.js +0 -1
  69. package/dist/ui/dist/assets/WebChatPage-CVRcKept.js +0 -16
  70. package/dist/ui/dist/assets/useProviders-C6_QHsEi.js +0 -1
@@ -194,13 +194,9 @@ export async function* runAgent(
194
194
  tool_calls: response.tool_calls,
195
195
  reasoning_content: response.reasoning_content,
196
196
  })
197
- if (!opts.isolated) {
198
- addMessage(opts.threadId, "assistant", response.content || "", {
199
- channel: opts.channel,
200
- tool_calls: response.tool_calls,
201
- reasoning_content: response.reasoning_content,
202
- })
203
- }
197
+ // Note: assistant messages with tool_calls are NOT persisted to DB.
198
+ // Only the final text response to the user is saved.
199
+ // Tool-call round-tripping happens in-memory via the 'messages' array above.
204
200
 
205
201
  for (const tc of response.tool_calls) {
206
202
  const toolName = tc.function.name
@@ -284,18 +280,12 @@ export async function* runAgent(
284
280
  await opts.onStep({ type: "tool_result", message: toolResultLLM })
285
281
  }
286
282
 
287
- // Add tool result to messages for next model call AND persist (TOON encoded)
283
+ // Add tool result to messages for next model call (in-memory only, NOT persisted to DB)
288
284
  messages.push({
289
285
  role: "tool",
290
286
  content: toolResultLLM,
291
287
  tool_call_id: tc.id,
292
288
  })
293
- if (!opts.isolated) {
294
- addMessage(opts.threadId, "tool", toolResultLLM, {
295
- channel: opts.channel,
296
- tool_call_id: tc.id,
297
- })
298
- }
299
289
 
300
290
  // Dynamic tool injection: when search_knowledge finds tools (native or MCP), add them to ctx.tools
301
291
  if (toolName === "search_knowledge") {
@@ -40,7 +40,7 @@ import { getUserDate, getUserTime } from "../utils/date"
40
40
  const log = logger.child("context-compiler")
41
41
 
42
42
  // Configuration constants
43
- const KEEP_LAST_N_MESSAGES = 15 // Always keep last N messages (Strategy: SELECT) — increased because tool calls/results are now persisted
43
+ const KEEP_LAST_N_MESSAGES = 30 // Always keep last N messages (Strategy: SELECT) — only user+assistant text, no tool results
44
44
  const DEFAULT_CONTEXT_WINDOW = 250000 // Default context window when model is unknown
45
45
  const COMPACT_RATIO = 0.80 // Compact when estimated input exceeds 70% of context window
46
46
  const MAX_SYSTEM_PROMPT_CHARS_CAP = 128000 // Hard cap for pathological prompts; normal budget is model-aware
@@ -97,7 +97,7 @@ export function getRecentMessages(threadId: string, n: number): StoredMessage[]
97
97
  const db = getDb()
98
98
  const rows = db.query(`
99
99
  SELECT * FROM conversations
100
- WHERE thread_id = ?
100
+ WHERE thread_id = ? AND role != 'tool'
101
101
  ORDER BY id DESC
102
102
  LIMIT ?
103
103
  `).all(threadId, n) as StoredMessage[]
@@ -170,10 +170,9 @@ export function toAPIMessages(rows: StoredMessage[]): LLMMessage[] {
170
170
  try { content = JSON.parse(r.content_multimodal) } catch { /* ignore */ }
171
171
  }
172
172
  const msg: LLMMessage = { role: r.role, content }
173
- if (r.tool_calls_json) {
174
- try { msg.tool_calls = JSON.parse(r.tool_calls_json) } catch { /* ignore */ }
175
- }
176
- if (r.tool_call_id) msg.tool_call_id = r.tool_call_id
173
+ // Note: tool_calls and tool_call_id are NOT reconstructed from DB.
174
+ // Tool results are kept in-memory during iteration but not persisted,
175
+ // so historical messages only contain text conversation.
177
176
  if (r.reasoning_content) msg.reasoning_content = r.reasoning_content
178
177
  return msg
179
178
  })
@@ -7,7 +7,6 @@
7
7
 
8
8
  import type { Config } from "../../config/loader.ts"
9
9
  import { logger } from "../../utils/logger.ts"
10
- import { getDb } from "../../storage/sqlite.ts"
11
10
  import { getAgentLoop } from "../agent-loop"
12
11
  import { resolveUserId, resolveAgentId } from "../../storage/onboarding"
13
12
  import type { ContentPart } from "../../multimodal/types"
@@ -34,6 +33,7 @@ export interface ModelOptions {
34
33
  onStep?: (step: StepEvent) => Promise<void>
35
34
  threadId?: string
36
35
  userId?: string
36
+ agentId?: string
37
37
  channel?: string
38
38
  rawUserMessage?: string
39
39
  signal?: AbortSignal
@@ -62,9 +62,8 @@ export class AgentRunner {
62
62
  }
63
63
 
64
64
  async generate(options: ModelOptions): Promise<ModelResponse> {
65
- const db = getDb()
66
- // Resolve agentId from database (coordinator or first enabled)
67
- const agentId = resolveAgentId(null) || "main"
65
+ // Resolve agentId from explicit option or database (coordinator/first enabled)
66
+ const agentId = options.agentId || resolveAgentId(null) || "main"
68
67
 
69
68
  // Resolve userId from database
70
69
  const userId = options.userId || resolveUserId({})
@@ -29,7 +29,6 @@
29
29
  *
30
30
  * 6. Tool categorization: Tools are categorized by semantic domain:
31
31
  * - scheduling (cron tools)
32
- * - projects (project/task management)
33
32
  * - filesystem (file operations)
34
33
  * - web (search/fetch)
35
34
  * - browser (browser automation)
@@ -527,10 +526,10 @@ function enrichToolDescription(tool: ToolDescriptor): string {
527
526
  *
528
527
  * Canonical format: `{safeServer}__{safeTool}` (double underscore as separator)
529
528
  */
530
- export function mcpToolFullName(_serverName: string, toolName: string): string {
529
+ export function mcpToolFullName(serverName: string, toolName: string): string {
531
530
  const safe = (s: string) => s.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_\-]/g, '_')
532
- const sanitized = safe(toolName)
533
- const trimmed = sanitized.length > 64 ? sanitized.substring(0, 64) : sanitized
531
+ const full = `${safe(serverName)}__${safe(toolName)}`
532
+ const trimmed = full.length > 64 ? full.substring(0, 64) : full
534
533
  return /^[a-zA-Z_]/.test(trimmed) ? trimmed : `_${trimmed}`.substring(0, 64)
535
534
  }
536
535
 
@@ -2,6 +2,7 @@ import { getDb } from "../storage/sqlite"
2
2
 
3
3
  export interface ResolveContextResult {
4
4
  userId: string
5
+ threadId: string
5
6
  agentId: string
6
7
  isNewUser: boolean
7
8
  }
@@ -51,8 +52,11 @@ export function resolveContext(options: ResolveContextOptions): ResolveContextRe
51
52
  .get()
52
53
 
53
54
  const agentId = coordinatorAgent?.id || "bee"
55
+ // One canonical conversation thread is shared across channels. Transport
56
+ // session IDs route replies; conversations.thread_id owns agent context.
57
+ const threadId = userId
54
58
 
55
- return { userId, agentId, isNewUser }
59
+ return { userId, threadId, agentId, isNewUser }
56
60
  }
57
61
 
58
62
  export function getDefaultAgentId(): string {
@@ -4,7 +4,7 @@
4
4
  * POST /api/chat
5
5
  * {
6
6
  * "message": "Mensaje para el coordinador",
7
- * "thread_id": "ID de sesión (opcional, se genera si no existe)",
7
+ * "thread_id": "conversations.thread_id (opcional, usa el thread canónico del usuario si no existe)",
8
8
  * "channel": "canal (opcional, default: webchat)"
9
9
  * }
10
10
  */
@@ -12,12 +12,11 @@
12
12
  import { getDb } from "../../storage/sqlite";
13
13
  import { resolveUserId, resolveAgentId } from "../../storage/onboarding";
14
14
  import { laneQueue } from "../lane-queue";
15
- import { getRecentMessages } from "../../agent/conversation-store";
16
15
  import { AgentRunner } from "../../agent/providers";
17
16
  import { logger } from "../../utils/logger";
18
- import { getUserDate, getUserTime } from "../../utils/date";
19
17
 
20
18
  const log = logger.child("api:chat");
19
+ export const DEFAULT_CHAT_HISTORY_LIMIT = 40;
21
20
 
22
21
  export interface ChatRequest {
23
22
  message: string;
@@ -34,6 +33,11 @@ export interface ChatResponse {
34
33
  error?: string;
35
34
  }
36
35
 
36
+ export function resolveChatThreadId(finalUserId: string, requestedThreadId?: string): string {
37
+ const trimmedThreadId = requestedThreadId?.trim();
38
+ return trimmedThreadId || finalUserId || "default";
39
+ }
40
+
37
41
  export async function handleChat(
38
42
  req: Request,
39
43
  addCorsHeaders: (res: Response, req: Request) => Response
@@ -60,8 +64,8 @@ export async function handleChat(
60
64
  // Resolve agent ID (coordinator by default)
61
65
  const finalAgentId = agentId || resolveAgentId(null) || "main";
62
66
 
63
- // Generate or use provided thread_id
64
- const threadId = thread_id || `${finalUserId}-${Date.now()}`;
67
+ // conversations.thread_id is the context key; never generate a per-request thread.
68
+ const threadId = resolveChatThreadId(finalUserId, thread_id);
65
69
 
66
70
  log.info(`[chat] Processing message from user=${finalUserId} agent=${finalAgentId} thread=${threadId}`);
67
71
 
@@ -86,15 +90,10 @@ export async function handleChat(
86
90
  // Format message with timestamp
87
91
  const messageContent = `[Timestamp: ${exactTime} (${userTimezone})]\n${message}`;
88
92
 
89
- // Get recent conversation history
90
- const history = getRecentMessages(threadId, 15);
91
- const messages = [
92
- ...history.map((row) => ({
93
- role: row.role as "user" | "assistant" | "system",
94
- content: row.content,
95
- })),
96
- { role: "user" as const, content: messageContent }
97
- ];
93
+ // AgentLoop persists this user message, then compileContext loads the last
94
+ // 15 messages from conversations by threadId. Prepending history here is
95
+ // ineffective because AgentLoop.stream only consumes the latest user input.
96
+ const messages = [{ role: "user" as const, content: messageContent }];
98
97
 
99
98
  // Get provider config from DB
100
99
  const agent = db.query<any, [string]>(
@@ -124,6 +123,7 @@ export async function handleChat(
124
123
  maxSteps: 15,
125
124
  threadId,
126
125
  userId: finalUserId,
126
+ agentId: finalAgentId,
127
127
  channel,
128
128
  onStep: async (step) => {
129
129
  if (step.type === "text" && step.message) {
@@ -200,12 +200,12 @@ export async function handleChat(
200
200
  export async function handleGetChatHistory(req: Request, addCorsHeaders: (r: Response, req: Request) => Response): Promise<Response> {
201
201
  const url = new URL(req.url)
202
202
  const threadId = url.searchParams.get("sessionId") || url.searchParams.get("threadId") || "default"
203
- const limit = parseInt(url.searchParams.get("limit") || "15")
203
+ const limit = parseInt(url.searchParams.get("limit") || String(DEFAULT_CHAT_HISTORY_LIMIT))
204
204
 
205
205
  const messages = getDb().query(`
206
206
  SELECT id, thread_id, channel, role, content, tool_calls_json, tool_call_id, reasoning_content, token_count, created_at, updated_at FROM conversations
207
207
  WHERE thread_id = ? AND role IN ('user', 'assistant')
208
- ORDER BY created_at DESC
208
+ ORDER BY id DESC
209
209
  LIMIT ?
210
210
  `).all(threadId, limit) as Record<string, unknown>[]
211
211
 
@@ -373,7 +373,7 @@ export async function startGateway(config: Config): Promise<void> {
373
373
 
374
374
  log.info(` Content: ${messageContent.substring(0, 150)}${messageContent.length > 150 ? "..." : ""}`);
375
375
 
376
- const { userId } = resolveContext({
376
+ const { userId, threadId: conversationThreadId } = resolveContext({
377
377
  channel: message.channel,
378
378
  channelUserId: message.sessionId,
379
379
  });
@@ -385,8 +385,8 @@ export async function startGateway(config: Config): Promise<void> {
385
385
  channelManager.startTyping(message.channel, message.sessionId),
386
386
  ]);
387
387
 
388
- // unifiedSessionId = userId del onboarding historial y thread LangGraph unificados
389
- const unifiedSessionId = userId;
388
+ // conversationThreadId = conversations.thread_id canónico compartido por todos los canales
389
+ const unifiedSessionId = conversationThreadId;
390
390
  // routingSessionId = peerId del canal → para enviar respuestas de vuelta al canal correcto
391
391
  const routingSessionId = message.sessionId;
392
392
 
@@ -1940,7 +1940,7 @@ export async function startGateway(config: Config): Promise<void> {
1940
1940
  return;
1941
1941
  }
1942
1942
  try {
1943
- const { userId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
1943
+ const { userId, threadId: conversationThreadId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
1944
1944
  const messages = [{ role: "user" as const, content: interactionMsg }];
1945
1945
  let streamedContent = "";
1946
1946
  const messageId = crypto.randomUUID();
@@ -1949,9 +1949,9 @@ export async function startGateway(config: Config): Promise<void> {
1949
1949
  provider: dbProvider as any,
1950
1950
  messages,
1951
1951
  maxTokens: 4096,
1952
- tools: prepareTools(agent, sessionId),
1952
+ tools: prepareTools(agent, conversationThreadId),
1953
1953
  maxSteps: 15,
1954
- threadId: sessionId,
1954
+ threadId: conversationThreadId,
1955
1955
  userId,
1956
1956
  onToken: async (token: string) => {
1957
1957
  if (signal.aborted) return;
@@ -2015,7 +2015,7 @@ export async function startGateway(config: Config): Promise<void> {
2015
2015
  return;
2016
2016
  }
2017
2017
  try {
2018
- const { userId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
2018
+ const { userId, threadId: conversationThreadId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
2019
2019
  const messages = [{ role: "user" as const, content: interactionMsg }];
2020
2020
  let streamedContent = "";
2021
2021
  const messageId = crypto.randomUUID();
@@ -2024,9 +2024,9 @@ export async function startGateway(config: Config): Promise<void> {
2024
2024
  provider: dbProvider as any,
2025
2025
  messages,
2026
2026
  maxTokens: 4096,
2027
- tools: prepareTools(agent, sessionId),
2027
+ tools: prepareTools(agent, conversationThreadId),
2028
2028
  maxSteps: 15,
2029
- threadId: sessionId,
2029
+ threadId: conversationThreadId,
2030
2030
  userId,
2031
2031
  onToken: async (token: string) => {
2032
2032
  if (signal.aborted) return;
@@ -2157,14 +2157,14 @@ export async function startGateway(config: Config): Promise<void> {
2157
2157
  }
2158
2158
 
2159
2159
  try {
2160
- const unifiedSessionId = msg.sessionId;
2161
- const messages = [{ role: "user" as const, content: messageContent }];
2162
- log.info(`Generating response for session ${unifiedSessionId}...`);
2163
-
2164
- const { userId } = resolveContext({
2160
+ const { userId, threadId: conversationThreadId } = resolveContext({
2165
2161
  channel: "webchat",
2166
2162
  channelUserId: msg.sessionId,
2167
2163
  });
2164
+ const unifiedSessionId = conversationThreadId;
2165
+ const routingSessionId = msg.sessionId;
2166
+ const messages = [{ role: "user" as const, content: messageContent }];
2167
+ log.info(`Generating response for session ${unifiedSessionId}...`);
2168
2168
 
2169
2169
  // Streaming: send tokens as they arrive
2170
2170
  let streamedContent = "";
@@ -2185,7 +2185,7 @@ export async function startGateway(config: Config): Promise<void> {
2185
2185
  ws.send(JSON.stringify({
2186
2186
  type: "message",
2187
2187
  id: messageId,
2188
- sessionId: unifiedSessionId,
2188
+ sessionId: routingSessionId,
2189
2189
  content: token,
2190
2190
  isChunk: true,
2191
2191
  isStep: false,
@@ -2200,7 +2200,7 @@ export async function startGateway(config: Config): Promise<void> {
2200
2200
  if (trimmedMessage) {
2201
2201
  ws.send(JSON.stringify({
2202
2202
  type: "progress",
2203
- sessionId: unifiedSessionId,
2203
+ sessionId: routingSessionId,
2204
2204
  content: trimmedMessage,
2205
2205
  } as OutboundMessage));
2206
2206
  }
@@ -2212,7 +2212,7 @@ export async function startGateway(config: Config): Promise<void> {
2212
2212
  const narration = getNarration(step.toolName);
2213
2213
  ws.send(JSON.stringify({
2214
2214
  type: "progress",
2215
- sessionId: unifiedSessionId,
2215
+ sessionId: routingSessionId,
2216
2216
  content: narration,
2217
2217
  } as OutboundMessage));
2218
2218
  return;
@@ -2227,7 +2227,7 @@ export async function startGateway(config: Config): Promise<void> {
2227
2227
  if (userMessage) {
2228
2228
  ws.send(JSON.stringify({
2229
2229
  type: "progress",
2230
- sessionId: unifiedSessionId,
2230
+ sessionId: routingSessionId,
2231
2231
  content: userMessage,
2232
2232
  } as OutboundMessage));
2233
2233
  }
@@ -2248,7 +2248,7 @@ export async function startGateway(config: Config): Promise<void> {
2248
2248
  let ttsProviderUsed: string | null = null;
2249
2249
  let ttsMimeType: string | null = null;
2250
2250
 
2251
- ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
2251
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: routingSessionId } as OutboundMessage));
2252
2252
 
2253
2253
  // Don't send text message if already streamed (content came via onToken)
2254
2254
  const alreadyStreamed = streamedContent.length > 0;
@@ -2258,7 +2258,7 @@ export async function startGateway(config: Config): Promise<void> {
2258
2258
  if (!voiceCfg.ttsProvider) {
2259
2259
  ws.send(JSON.stringify({
2260
2260
  type: "message",
2261
- sessionId: unifiedSessionId,
2261
+ sessionId: routingSessionId,
2262
2262
  content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
2263
2263
  isStep: false,
2264
2264
  } as OutboundMessage));
@@ -2273,7 +2273,7 @@ export async function startGateway(config: Config): Promise<void> {
2273
2273
  log.info(`Audio generated: ${base64Audio.length} bytes, mimeType: ${audioOutput.mimeType}`);
2274
2274
  ws.send(JSON.stringify({
2275
2275
  type: "message",
2276
- sessionId: unifiedSessionId,
2276
+ sessionId: routingSessionId,
2277
2277
  content,
2278
2278
  audio: base64Audio,
2279
2279
  mimeType: audioOutput.mimeType,
@@ -2281,11 +2281,11 @@ export async function startGateway(config: Config): Promise<void> {
2281
2281
  } as OutboundMessage));
2282
2282
  } catch (ttsError) {
2283
2283
  log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
2284
- ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2284
+ ws.send(JSON.stringify({ type: "message", sessionId: routingSessionId, content, isStep: false } as OutboundMessage));
2285
2285
  }
2286
2286
  }
2287
2287
  } else {
2288
- ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2288
+ ws.send(JSON.stringify({ type: "message", sessionId: routingSessionId, content, isStep: false } as OutboundMessage));
2289
2289
  }
2290
2290
  } else if (alreadyStreamed && shouldSpeak && voiceCfg.ttsProvider) {
2291
2291
  try {
@@ -2295,7 +2295,7 @@ export async function startGateway(config: Config): Promise<void> {
2295
2295
  log.info(`Audio generated after streaming: ${base64Audio.length} bytes`);
2296
2296
  ws.send(JSON.stringify({
2297
2297
  type: "message",
2298
- sessionId: unifiedSessionId,
2298
+ sessionId: routingSessionId,
2299
2299
  content,
2300
2300
  audio: base64Audio,
2301
2301
  mimeType: audioOutput.mimeType,
@@ -2349,7 +2349,12 @@ export async function startGateway(config: Config): Promise<void> {
2349
2349
  }
2350
2350
 
2351
2351
  try {
2352
- const unifiedSessionId = msg.sessionId;
2352
+ const { userId, threadId: conversationThreadId } = resolveContext({
2353
+ channel: "webchat",
2354
+ channelUserId: msg.sessionId,
2355
+ });
2356
+ const unifiedSessionId = conversationThreadId;
2357
+ const routingSessionId = msg.sessionId;
2353
2358
 
2354
2359
  // Multimodal: process image/document if present
2355
2360
  let finalMessageContent = msg.content;
@@ -2418,11 +2423,6 @@ export async function startGateway(config: Config): Promise<void> {
2418
2423
 
2419
2424
  log.info(`Generating response for session ${unifiedSessionId} (multimodal: ${!!(msg.image || msg.document)})...`);
2420
2425
 
2421
- const { userId } = resolveContext({
2422
- channel: "webchat",
2423
- channelUserId: msg.sessionId,
2424
- });
2425
-
2426
2426
  // Streaming: send tokens as they arrive
2427
2427
  let streamedContent = "";
2428
2428
  let messageId = crypto.randomUUID();
@@ -2443,7 +2443,7 @@ export async function startGateway(config: Config): Promise<void> {
2443
2443
  ws.send(JSON.stringify({
2444
2444
  type: "message",
2445
2445
  id: messageId,
2446
- sessionId: unifiedSessionId,
2446
+ sessionId: routingSessionId,
2447
2447
  content: token,
2448
2448
  isChunk: true,
2449
2449
  isStep: false,
@@ -2458,7 +2458,7 @@ export async function startGateway(config: Config): Promise<void> {
2458
2458
  if (trimmedMessage) {
2459
2459
  ws.send(JSON.stringify({
2460
2460
  type: "progress",
2461
- sessionId: unifiedSessionId,
2461
+ sessionId: routingSessionId,
2462
2462
  content: trimmedMessage,
2463
2463
  } as OutboundMessage));
2464
2464
  }
@@ -2470,7 +2470,7 @@ export async function startGateway(config: Config): Promise<void> {
2470
2470
  const narration = getNarration(step.toolName);
2471
2471
  ws.send(JSON.stringify({
2472
2472
  type: "progress",
2473
- sessionId: unifiedSessionId,
2473
+ sessionId: routingSessionId,
2474
2474
  content: narration,
2475
2475
  } as OutboundMessage));
2476
2476
  return;
@@ -2485,7 +2485,7 @@ export async function startGateway(config: Config): Promise<void> {
2485
2485
  if (userMessage) {
2486
2486
  ws.send(JSON.stringify({
2487
2487
  type: "progress",
2488
- sessionId: unifiedSessionId,
2488
+ sessionId: routingSessionId,
2489
2489
  content: userMessage,
2490
2490
  } as OutboundMessage));
2491
2491
  }
@@ -2506,7 +2506,7 @@ export async function startGateway(config: Config): Promise<void> {
2506
2506
  let ttsProviderUsed: string | null = null;
2507
2507
  let ttsMimeType: string | null = null;
2508
2508
 
2509
- ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
2509
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: routingSessionId } as OutboundMessage));
2510
2510
 
2511
2511
  // Don't send text message if already streamed (content came via onToken)
2512
2512
  const alreadyStreamed = streamedContent.length > 0;
@@ -2516,7 +2516,7 @@ export async function startGateway(config: Config): Promise<void> {
2516
2516
  if (!voiceConfig.ttsProvider) {
2517
2517
  ws.send(JSON.stringify({
2518
2518
  type: "message",
2519
- sessionId: unifiedSessionId,
2519
+ sessionId: routingSessionId,
2520
2520
  content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
2521
2521
  isStep: false
2522
2522
  } as OutboundMessage));
@@ -2530,7 +2530,7 @@ export async function startGateway(config: Config): Promise<void> {
2530
2530
  const base64Audio = (audioOutput.data as Buffer).toString("base64");
2531
2531
  ws.send(JSON.stringify({
2532
2532
  type: "message",
2533
- sessionId: unifiedSessionId,
2533
+ sessionId: routingSessionId,
2534
2534
  content,
2535
2535
  audio: base64Audio,
2536
2536
  mimeType: audioOutput.mimeType,
@@ -2538,11 +2538,11 @@ export async function startGateway(config: Config): Promise<void> {
2538
2538
  } as OutboundMessage));
2539
2539
  } catch (ttsError) {
2540
2540
  log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
2541
- ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2541
+ ws.send(JSON.stringify({ type: "message", sessionId: routingSessionId, content, isStep: false } as OutboundMessage));
2542
2542
  }
2543
2543
  }
2544
2544
  } else {
2545
- ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
2545
+ ws.send(JSON.stringify({ type: "message", sessionId: routingSessionId, content, isStep: false } as OutboundMessage));
2546
2546
  }
2547
2547
  } else if (alreadyStreamed && shouldSpeak && voiceConfig.ttsProvider) {
2548
2548
  try {
@@ -2552,7 +2552,7 @@ export async function startGateway(config: Config): Promise<void> {
2552
2552
  log.info(`Audio generated after streaming: ${base64Audio.length} bytes`);
2553
2553
  ws.send(JSON.stringify({
2554
2554
  type: "message",
2555
- sessionId: unifiedSessionId,
2555
+ sessionId: routingSessionId,
2556
2556
  content,
2557
2557
  audio: base64Audio,
2558
2558
  mimeType: audioOutput.mimeType,
@@ -2563,15 +2563,14 @@ export async function startGateway(config: Config): Promise<void> {
2563
2563
  }
2564
2564
  }
2565
2565
  } catch (error) {
2566
- const unifiedSessionId = msg.sessionId;
2567
2566
  // Detener typing aunque falle — nunca dejar el spinner infinito
2568
- ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
2567
+ ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
2569
2568
  ws.send(JSON.stringify({
2570
2569
  type: "error",
2571
- sessionId: unifiedSessionId,
2570
+ sessionId: msg.sessionId,
2572
2571
  error: (error as Error).message,
2573
2572
  } as OutboundMessage));
2574
- log.error(`Error for session ${unifiedSessionId}: ${(error as Error).message}`);
2573
+ log.error(`Error for session ${msg.sessionId}: ${(error as Error).message}`);
2575
2574
  }
2576
2575
  });
2577
2576
 
@@ -2741,4 +2740,4 @@ export async function startGateway(config: Config): Promise<void> {
2741
2740
  log.error(`Failed to reload configuration: ${(error as Error).message}`);
2742
2741
  }
2743
2742
  });
2744
- }
2743
+ }