@sunnoy/wecom 2.2.0 → 2.3.0

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 (44) hide show
  1. package/README.md +2 -1
  2. package/index.js +13 -2
  3. package/openclaw.plugin.json +137 -1
  4. package/package.json +2 -3
  5. package/skills/wecom-contact-lookup/SKILL.md +167 -0
  6. package/skills/wecom-doc-manager/SKILL.md +106 -0
  7. package/skills/wecom-doc-manager/references/api-create-doc.md +56 -0
  8. package/skills/wecom-doc-manager/references/api-edit-doc-content.md +68 -0
  9. package/skills/wecom-doc-manager/references/api-export-document.md +88 -0
  10. package/skills/wecom-edit-todo/SKILL.md +254 -0
  11. package/skills/wecom-get-todo-detail/SKILL.md +148 -0
  12. package/skills/wecom-get-todo-list/SKILL.md +132 -0
  13. package/skills/wecom-meeting-create/SKILL.md +163 -0
  14. package/skills/wecom-meeting-create/references/example-full.md +30 -0
  15. package/skills/wecom-meeting-create/references/example-reminder.md +46 -0
  16. package/skills/wecom-meeting-create/references/example-security.md +22 -0
  17. package/skills/wecom-meeting-manage/SKILL.md +141 -0
  18. package/skills/wecom-meeting-query/SKILL.md +335 -0
  19. package/skills/wecom-preflight/SKILL.md +103 -0
  20. package/skills/wecom-schedule/SKILL.md +164 -0
  21. package/skills/wecom-schedule/references/api-check-availability.md +56 -0
  22. package/skills/wecom-schedule/references/api-create-schedule.md +38 -0
  23. package/skills/wecom-schedule/references/api-get-schedule-detail.md +81 -0
  24. package/skills/wecom-schedule/references/api-update-schedule.md +30 -0
  25. package/skills/wecom-schedule/references/ref-reminders.md +24 -0
  26. package/skills/wecom-smartsheet-data/SKILL.md +76 -0
  27. package/skills/wecom-smartsheet-data/references/api-get-records.md +61 -0
  28. package/skills/wecom-smartsheet-data/references/cell-value-formats.md +120 -0
  29. package/skills/wecom-smartsheet-schema/SKILL.md +96 -0
  30. package/skills/wecom-smartsheet-schema/references/field-types.md +43 -0
  31. package/wecom/accounts.js +2 -0
  32. package/wecom/callback-inbound.js +9 -5
  33. package/wecom/channel-plugin.js +65 -1
  34. package/wecom/constants.js +18 -7
  35. package/wecom/image-studio-tool.js +764 -0
  36. package/wecom/mcp-tool.js +660 -0
  37. package/wecom/parent-resolver.js +26 -0
  38. package/wecom/plugin-config.js +484 -0
  39. package/wecom/target.js +3 -2
  40. package/wecom/welcome-messages-file.js +155 -0
  41. package/wecom/workspace-template.js +40 -4
  42. package/wecom/ws-monitor.js +186 -12
  43. package/skills/wecom-doc/SKILL.md +0 -363
  44. package/skills/wecom-doc/references/doc-api.md +0 -224
@@ -220,7 +220,7 @@ export function seedAgentWorkspace(agentId, config, overrideTemplateDir) {
220
220
  }
221
221
  }
222
222
 
223
- export function upsertAgentIdOnlyEntry(cfg, agentId) {
223
+ export function upsertAgentIdOnlyEntry(cfg, agentId, baseAgentId) {
224
224
  const normalizedId = String(agentId || "")
225
225
  .trim()
226
226
  .toLowerCase();
@@ -252,8 +252,44 @@ export function upsertAgentIdOnlyEntry(cfg, agentId) {
252
252
  }
253
253
 
254
254
  if (!existingIds.has(normalizedId)) {
255
- nextList.push({ id: normalizedId, heartbeat: {} });
255
+ const entry = { id: normalizedId, heartbeat: {} };
256
+
257
+ // Inherit inheritable properties from the base agent so the dynamic
258
+ // agent retains model, subagents (spawn permissions), and tool config.
259
+ if (baseAgentId) {
260
+ const baseEntry = currentList.find(
261
+ (e) => e && typeof e.id === "string" && e.id === baseAgentId,
262
+ );
263
+ if (baseEntry) {
264
+ for (const key of ["model", "subagents", "tools"]) {
265
+ if (baseEntry[key] != null) {
266
+ entry[key] = JSON.parse(JSON.stringify(baseEntry[key]));
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ nextList.push(entry);
256
273
  changed = true;
274
+ } else if (baseAgentId) {
275
+ // Backfill missing inheritable properties on existing entries that were
276
+ // persisted before the inheritance logic was added.
277
+ const existingEntry = nextList.find(
278
+ (e) => e && typeof e.id === "string" && e.id.trim().toLowerCase() === normalizedId,
279
+ );
280
+ if (existingEntry) {
281
+ const baseEntry = currentList.find(
282
+ (e) => e && typeof e.id === "string" && e.id === baseAgentId,
283
+ );
284
+ if (baseEntry) {
285
+ for (const key of ["model", "subagents", "tools"]) {
286
+ if (existingEntry[key] == null && baseEntry[key] != null) {
287
+ existingEntry[key] = JSON.parse(JSON.stringify(baseEntry[key]));
288
+ changed = true;
289
+ }
290
+ }
291
+ }
292
+ }
257
293
  }
258
294
 
259
295
  if (changed) {
@@ -263,7 +299,7 @@ export function upsertAgentIdOnlyEntry(cfg, agentId) {
263
299
  return changed;
264
300
  }
265
301
 
266
- export async function ensureDynamicAgentListed(agentId, templateDir) {
302
+ export async function ensureDynamicAgentListed(agentId, templateDir, baseAgentId) {
267
303
  const normalizedId = String(agentId || "")
268
304
  .trim()
269
305
  .toLowerCase();
@@ -285,7 +321,7 @@ export async function ensureDynamicAgentListed(agentId, templateDir) {
285
321
  }
286
322
 
287
323
  // Upsert into in-memory config so the running gateway sees it immediately.
288
- const changed = upsertAgentIdOnlyEntry(openclawConfig, normalizedId);
324
+ const changed = upsertAgentIdOnlyEntry(openclawConfig, normalizedId, baseAgentId);
289
325
  if (changed) {
290
326
  logger.info("WeCom: dynamic agent added to in-memory agents.list", { agentId: normalizedId });
291
327
 
@@ -30,6 +30,7 @@ import {
30
30
  MEDIA_IMAGE_PLACEHOLDER,
31
31
  MESSAGE_PROCESS_TIMEOUT_MS,
32
32
  REPLY_SEND_TIMEOUT_MS,
33
+ STREAM_MAX_LIFETIME_MS,
33
34
  THINKING_MESSAGE,
34
35
  WS_HEARTBEAT_INTERVAL_MS,
35
36
  WS_MAX_RECONNECT_ATTEMPTS,
@@ -62,6 +63,8 @@ import {
62
63
  startMessageStateCleanup,
63
64
  } from "./ws-state.js";
64
65
  import { ensureDynamicAgentListed } from "./workspace-template.js";
66
+ import { listAccountIds, resolveAccount } from "./accounts.js";
67
+ import { loadWelcomeMessagesFromFile } from "./welcome-messages-file.js";
65
68
 
66
69
  const DEFAULT_AGENT_ID = "main";
67
70
  const DEFAULT_STATE_DIRNAME = ".openclaw";
@@ -72,8 +75,9 @@ const VISIBLE_STREAM_THROTTLE_MS = 800;
72
75
  // Reserve headroom below the SDK's per-reqId queue limit (100) so the final
73
76
  // reply always has room.
74
77
  const MAX_INTERMEDIATE_STREAM_MESSAGES = 85;
75
- // WeCom stream messages expire if not updated within 6 minutes. Send a
76
- // keepalive update every 4 minutes to keep the stream alive during long runs.
78
+ // WeCom stream messages have a hard 6-minute absolute lifetime from creation.
79
+ // Keepalive updates every 4 minutes maintain visible progress but do NOT extend
80
+ // the lifetime. Stream rotation (see rotateStream) is the actual fix.
77
81
  const STREAM_KEEPALIVE_INTERVAL_MS = 4 * 60 * 1000;
78
82
  // Match MEDIA:/FILE: directives at line start, optionally preceded by markdown list markers.
79
83
  const REPLY_MEDIA_DIRECTIVE_PATTERN = /^\s*(?:[-*•]\s+|\d+\.\s+)?(?:MEDIA|FILE)\s*:/im;
@@ -136,6 +140,11 @@ function buildWaitingModelContent(seconds) {
136
140
  return `<think>${lines.join("\n")}`;
137
141
  }
138
142
 
143
+ function buildWaitingModelReasoningText(seconds) {
144
+ const normalizedSeconds = Math.max(1, Number.parseInt(String(seconds ?? 1), 10) || 1);
145
+ return `等待模型响应 ${normalizedSeconds}s`;
146
+ }
147
+
139
148
  function buildWsStreamContent({ reasoningText = "", visibleText = "", finish = false }) {
140
149
  const normalizedReasoning = String(reasoningText ?? "").trim();
141
150
  const normalizedVisible = String(visibleText ?? "").trim();
@@ -357,10 +366,31 @@ function resolveAgentWorkspaceDir(config, agentId) {
357
366
  return path.join(stateDir, `workspace-${normalizedAgentId}`);
358
367
  }
359
368
 
369
+ function resolveConfiguredReplyMediaLocalRoots(config) {
370
+ const topLevel = Array.isArray(config?.channels?.[CHANNEL_ID]?.mediaLocalRoots)
371
+ ? config.channels[CHANNEL_ID].mediaLocalRoots
372
+ : [];
373
+
374
+ // Also collect mediaLocalRoots from each account entry (multi-account mode).
375
+ // In single-account mode the account config IS the top-level config, so
376
+ // listAccountIds returns ["default"] and the roots are already in topLevel.
377
+ const accountRoots = [];
378
+ for (const accountId of listAccountIds(config)) {
379
+ const accountConfig = resolveAccount(config, accountId)?.config;
380
+ if (Array.isArray(accountConfig?.mediaLocalRoots)) {
381
+ accountRoots.push(...accountConfig.mediaLocalRoots);
382
+ }
383
+ }
384
+
385
+ const merged = [...new Set([...topLevel, ...accountRoots])];
386
+ return merged.map((entry) => resolveUserPath(entry)).filter(Boolean);
387
+ }
388
+
360
389
  function resolveReplyMediaLocalRoots(config, agentId) {
361
390
  const workspaceDir = resolveAgentWorkspaceDir(config, agentId || resolveDefaultAgentId(config));
362
391
  const browserMediaDir = path.join(resolveStateDir(), "media", "browser");
363
- return [...new Set([workspaceDir, browserMediaDir].map((entry) => path.resolve(entry)))];
392
+ const configuredRoots = resolveConfiguredReplyMediaLocalRoots(config);
393
+ return [...new Set([workspaceDir, browserMediaDir, ...configuredRoots].map((entry) => path.resolve(entry)))];
364
394
  }
365
395
 
366
396
  function mergeReplyMediaUrls(...lists) {
@@ -387,12 +417,13 @@ function mergeReplyMediaUrls(...lists) {
387
417
  function buildReplyMediaGuidance(config, agentId) {
388
418
  const workspaceDir = resolveAgentWorkspaceDir(config, agentId || resolveDefaultAgentId(config));
389
419
  const browserMediaDir = path.join(resolveStateDir(), "media", "browser");
390
- return [
420
+ const configuredRoots = resolveConfiguredReplyMediaLocalRoots(config);
421
+ const qwenImageToolsConfig = config?.plugins?.entries?.wecom?.config?.qwenImageTools;
422
+ const guidance = [
391
423
  WECOM_REPLY_MEDIA_GUIDANCE_HEADER,
392
424
  `Local reply files are allowed only under the current workspace: ${workspaceDir}`,
393
425
  "Inside the agent sandbox, that same workspace is visible as /workspace.",
394
426
  `Browser-generated files are also allowed only under: ${browserMediaDir}`,
395
- "Never reference any other host path.",
396
427
  "Do NOT call message.send or message.sendAttachment to deliver files back to the current WeCom chat/user; use MEDIA: or FILE: directives instead.",
397
428
  "For images: put each image path on its own line as MEDIA:/abs/path.",
398
429
  "If a local file is in the current sandbox workspace, use its /workspace/... path directly.",
@@ -402,7 +433,27 @@ function buildReplyMediaGuidance(config, agentId) {
402
433
  "CRITICAL: If a tool already returned a path prefixed with FILE: (e.g. FILE:/abs/path.pdf), keep the FILE: prefix exactly as-is. Do NOT change it to MEDIA:.",
403
434
  "Each directive MUST be on its own line with no other text on that line.",
404
435
  "The plugin will automatically send the media to the user.",
405
- ].join("\n");
436
+ ];
437
+
438
+ if (configuredRoots.length > 0) {
439
+ guidance.push(`Additional configured host roots are also allowed: ${configuredRoots.join(", ")}`);
440
+ }
441
+
442
+ if (qwenImageToolsConfig?.enabled === true) {
443
+ guidance.push(
444
+ "[WeCom image_studio rule]",
445
+ "When the user asks to generate an image, use image_studio with action=\"generate\".",
446
+ "When the user asks to edit an existing image, use image_studio with action=\"edit\" and pass images.",
447
+ "Use aspect=\"landscape\" for architecture diagrams, flowcharts, and banners unless the user asks otherwise.",
448
+ "Prefer model_preference=\"qwen\" for text-heavy diagrams or label-rich images, and model_preference=\"wan\" for photorealistic scenes.",
449
+ "For workspace-local images, always use /workspace/... paths when calling image_studio.",
450
+ "Prefer n=1 unless the user explicitly asks for multiple images.",
451
+ "If image_studio returns MEDIA: URLs, treat the image task as completed successfully.",
452
+ );
453
+ }
454
+
455
+ guidance.push("Never reference any other host path.");
456
+ return guidance.join("\n");
406
457
  }
407
458
 
408
459
  function normalizeReplyMediaUrlForLoad(mediaUrl, config, agentId) {
@@ -546,6 +597,11 @@ async function sendMediaBatch({ wsClient, frame, state, account, runtime, config
546
597
 
547
598
  if (result.ok) {
548
599
  state.hasMedia = true;
600
+ if (result.finalType === "image") {
601
+ state.hasImageMedia = true;
602
+ } else {
603
+ state.hasFileMedia = true;
604
+ }
549
605
  if (result.downgraded) {
550
606
  logger.info(`[WS] Media downgraded: ${result.downgradeNote}`);
551
607
  }
@@ -575,8 +631,28 @@ async function finishThinkingStream({ wsClient, frame, state, accountId }) {
575
631
  visibleText: finalVisibleText,
576
632
  finish: true,
577
633
  });
634
+ } else if (state.reasoningText) {
635
+ // If the model only emitted reasoning tokens, close the thinking stream
636
+ // instead of replacing it with a generic completion stub.
637
+ finishText = buildWsStreamContent({
638
+ reasoningText: state.reasoningText,
639
+ visibleText: "",
640
+ finish: true,
641
+ });
578
642
  } else if (state.hasMedia) {
579
- finishText = "文件已发送,请查收。";
643
+ const mediaVisibleText = state.hasImageMedia && !state.hasFileMedia
644
+ ? "图片已生成,请查收。"
645
+ : "文件已发送,请查收。";
646
+ const fallbackReasoningText = state.waitingModelSeconds > 0
647
+ ? buildWaitingModelReasoningText(state.waitingModelSeconds)
648
+ : state.hasImageMedia && !state.hasFileMedia
649
+ ? "正在生成图片"
650
+ : "正在整理并发送文件";
651
+ finishText = buildWsStreamContent({
652
+ reasoningText: fallbackReasoningText,
653
+ visibleText: mediaVisibleText,
654
+ finish: true,
655
+ });
580
656
  } else if (state.hasMediaFailed && state.mediaErrorSummary) {
581
657
  finishText = state.mediaErrorSummary;
582
658
  } else {
@@ -599,6 +675,12 @@ function resolveWelcomeMessage(account) {
599
675
  return configured;
600
676
  }
601
677
 
678
+ const fromFile = loadWelcomeMessagesFromFile(account?.config);
679
+ if (fromFile?.length) {
680
+ const pick = Math.floor(Math.random() * fromFile.length);
681
+ return fromFile[pick];
682
+ }
683
+
602
684
  const index = Math.floor(Math.random() * DEFAULT_WELCOME_MESSAGES.length);
603
685
  return DEFAULT_WELCOME_MESSAGES[index] || DEFAULT_WELCOME_MESSAGE;
604
686
  }
@@ -1072,6 +1154,14 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1072
1154
  return;
1073
1155
  }
1074
1156
  text = extractGroupMessageContent(originalText, account.config);
1157
+ if (!text.trim() && imageUrls.length === 0 && fileUrls.length === 0) {
1158
+ logger.debug("[WS] Group message mention stripped to empty content; skipping reply", {
1159
+ accountId: account.accountId,
1160
+ chatId,
1161
+ senderId,
1162
+ });
1163
+ return;
1164
+ }
1075
1165
  }
1076
1166
 
1077
1167
  const senderIsAdmin = isWecomAdmin(senderId, account.config);
@@ -1123,12 +1213,16 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1123
1213
  accumulatedText: "",
1124
1214
  reasoningText: "",
1125
1215
  streamId,
1216
+ streamCreatedAt: Date.now(),
1126
1217
  replyMediaUrls: [],
1127
1218
  pendingMediaUrls: [],
1128
1219
  hasMedia: false,
1220
+ hasImageMedia: false,
1221
+ hasFileMedia: false,
1129
1222
  hasMediaFailed: false,
1130
1223
  mediaErrorSummary: "",
1131
1224
  deliverCalled: false,
1225
+ waitingModelSeconds: 0,
1132
1226
  };
1133
1227
  setMessageState(messageId, state);
1134
1228
 
@@ -1143,6 +1237,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1143
1237
  let lastNonEmptyStreamText = "";
1144
1238
  let lastForwardedVisibleText = "";
1145
1239
  let keepaliveTimer = null;
1240
+ let rotationTimer = null;
1146
1241
  let waitingModelTimer = null;
1147
1242
  let waitingModelSeconds = 0;
1148
1243
  let waitingModelActive = false;
@@ -1159,6 +1254,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1159
1254
 
1160
1255
  const sendWaitingModelUpdate = async (seconds) => {
1161
1256
  const waitingText = buildWaitingModelContent(seconds);
1257
+ state.waitingModelSeconds = seconds;
1162
1258
  lastStreamSentAt = Date.now();
1163
1259
  lastNonEmptyStreamText = waitingText;
1164
1260
  try {
@@ -1391,6 +1487,75 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1391
1487
  }, STREAM_KEEPALIVE_INTERVAL_MS);
1392
1488
  };
1393
1489
 
1490
+ // --- Stream rotation: finish the current stream before the 6-minute hard
1491
+ // limit and seamlessly continue on a new streamId. ----
1492
+
1493
+ const rotateStream = async () => {
1494
+ // Flush any pending throttled updates to the current stream first.
1495
+ await flushPendingStreamUpdates();
1496
+
1497
+ const oldStreamId = state.streamId;
1498
+ const hasVisibleText = Boolean(stripThinkTags(state.accumulatedText));
1499
+
1500
+ // Build finish content. When still in the thinking phase (no visible
1501
+ // text yet), append a small marker so the finished message is not empty.
1502
+ const finishText = buildWsStreamContent({
1503
+ reasoningText: state.reasoningText,
1504
+ visibleText: hasVisibleText ? state.accumulatedText : "⏳ 处理中…",
1505
+ finish: true,
1506
+ });
1507
+
1508
+ try {
1509
+ streamMessagesSent++;
1510
+ await sendWsReply({
1511
+ wsClient,
1512
+ frame,
1513
+ streamId: oldStreamId,
1514
+ text: finishText,
1515
+ finish: true,
1516
+ accountId: account.accountId,
1517
+ });
1518
+ } catch (err) {
1519
+ logger.warn(`[WS] Stream rotation: failed to finish old stream: ${err.message}`);
1520
+ }
1521
+
1522
+ // Switch to new stream.
1523
+ const newStreamId = generateReqId("stream");
1524
+ state.streamId = newStreamId;
1525
+ state.streamCreatedAt = Date.now();
1526
+ state.accumulatedText = "";
1527
+ state.reasoningText = "";
1528
+
1529
+ // Reset per-stream counters.
1530
+ streamMessagesSent = 0;
1531
+ lastStreamSentAt = Date.now();
1532
+ lastReasoningSendAt = 0;
1533
+ lastVisibleSendAt = 0;
1534
+ lastNonEmptyStreamText = "";
1535
+ lastForwardedVisibleText = "";
1536
+
1537
+ if (reqIdStore) reqIdStore.set(chatId, newStreamId);
1538
+
1539
+ logPerf("stream_rotated", { oldStreamId, newStreamId });
1540
+
1541
+ // Re-arm timers for the new stream.
1542
+ scheduleRotation();
1543
+ scheduleKeepalive();
1544
+ };
1545
+
1546
+ const scheduleRotation = () => {
1547
+ if (rotationTimer) clearTimeout(rotationTimer);
1548
+ const remaining = STREAM_MAX_LIFETIME_MS - (Date.now() - state.streamCreatedAt);
1549
+ if (remaining <= 0) {
1550
+ void rotateStream();
1551
+ return;
1552
+ }
1553
+ rotationTimer = setTimeout(() => {
1554
+ rotationTimer = null;
1555
+ void rotateStream();
1556
+ }, remaining);
1557
+ };
1558
+
1394
1559
  const cancelPendingTimers = () => {
1395
1560
  if (pendingReasoningTimer) {
1396
1561
  clearTimeout(pendingReasoningTimer);
@@ -1404,6 +1569,10 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1404
1569
  clearTimeout(keepaliveTimer);
1405
1570
  keepaliveTimer = null;
1406
1571
  }
1572
+ if (rotationTimer) {
1573
+ clearTimeout(rotationTimer);
1574
+ rotationTimer = null;
1575
+ }
1407
1576
  stopWaitingModelUpdates();
1408
1577
  };
1409
1578
 
@@ -1415,6 +1584,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1415
1584
  if (account.sendThinkingMessage !== false) {
1416
1585
  waitingModelActive = true;
1417
1586
  waitingModelSeconds = 1;
1587
+ state.waitingModelSeconds = waitingModelSeconds;
1418
1588
  await sendThinkingReply({
1419
1589
  wsClient,
1420
1590
  frame,
@@ -1428,6 +1598,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1428
1598
  }
1429
1599
  lastStreamSentAt = Date.now();
1430
1600
  scheduleKeepalive();
1601
+ scheduleRotation();
1431
1602
 
1432
1603
  const peerKind = isGroupChat ? "group" : "dm";
1433
1604
  const peerId = isGroupChat ? chatId : senderId;
@@ -1438,10 +1609,6 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1438
1609
  ? generateAgentId(peerKind, peerId, account.accountId)
1439
1610
  : null;
1440
1611
 
1441
- if (dynamicAgentId) {
1442
- await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate);
1443
- }
1444
-
1445
1612
  const route = core.routing.resolveAgentRoute({
1446
1613
  cfg: config,
1447
1614
  channel: CHANNEL_ID,
@@ -1456,8 +1623,15 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
1456
1623
  );
1457
1624
 
1458
1625
  if (dynamicAgentId && !hasExplicitBinding) {
1626
+ const routeAgentId = route.agentId;
1627
+ // Use the account's configured agentId as the base for property inheritance
1628
+ // (model, subagents, tools). route.agentId may resolve to "main" when
1629
+ // there is no explicit binding, but the account's agentId points to the
1630
+ // actual parent agent whose properties the dynamic agent should inherit.
1631
+ const baseAgentId = account.config.agentId || routeAgentId;
1632
+ await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate, baseAgentId);
1633
+ route.sessionKey = route.sessionKey.replace(`agent:${routeAgentId}:`, `agent:${dynamicAgentId}:`);
1459
1634
  route.agentId = dynamicAgentId;
1460
- route.sessionKey = `agent:${dynamicAgentId}:${peerKind}:${peerId}`;
1461
1635
  }
1462
1636
 
1463
1637
  const { ctxPayload, storePath } = buildInboundContext({