@ynhcj/xiaoyi-channel 0.0.1-release2026060601 → 0.0.1-task

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 (127) hide show
  1. package/dist/index.js +159 -29
  2. package/dist/src/bot.js +2 -44
  3. package/dist/src/channel.js +5 -8
  4. package/dist/src/config-schema.d.ts +13 -0
  5. package/dist/src/config-schema.js +11 -0
  6. package/dist/src/config.js +5 -0
  7. package/dist/src/conversation/conversation-manager.d.ts +96 -0
  8. package/dist/src/conversation/conversation-manager.js +472 -0
  9. package/dist/src/conversation/conversation-session.d.ts +60 -0
  10. package/dist/src/conversation/conversation-session.js +22 -0
  11. package/dist/src/conversation/cron-buffer.d.ts +50 -0
  12. package/dist/src/conversation/cron-buffer.js +269 -0
  13. package/dist/src/conversation/outbound-gateway.d.ts +43 -0
  14. package/dist/src/conversation/outbound-gateway.js +81 -0
  15. package/dist/src/conversation/outbound-queue.d.ts +35 -0
  16. package/dist/src/conversation/outbound-queue.js +96 -0
  17. package/dist/src/conversation/steer-service.d.ts +29 -0
  18. package/dist/src/conversation/steer-service.js +52 -0
  19. package/dist/src/conversation/stream-assembler.d.ts +52 -0
  20. package/dist/src/conversation/stream-assembler.js +122 -0
  21. package/dist/src/cron-command.js +2 -24
  22. package/dist/src/cspl/call_api.js +8 -2
  23. package/dist/src/cspl/constants.d.ts +1 -0
  24. package/dist/src/cspl/constants.js +2 -0
  25. package/dist/src/cspl/sentinel_hook.js +9 -12
  26. package/dist/src/cspl/upload_file.js +1 -1
  27. package/dist/src/cspl/utils.d.ts +2 -2
  28. package/dist/src/cspl/utils.js +16 -16
  29. package/dist/src/dispatch/bot.d.ts +20 -0
  30. package/dist/src/dispatch/bot.js +616 -0
  31. package/dist/src/dispatch/monitor.d.ts +17 -0
  32. package/dist/src/dispatch/monitor.js +333 -0
  33. package/dist/src/dispatch/outbound.d.ts +7 -0
  34. package/dist/src/dispatch/outbound.js +283 -0
  35. package/dist/src/dispatch/reply-dispatcher.d.ts +22 -0
  36. package/dist/src/dispatch/reply-dispatcher.js +633 -0
  37. package/dist/src/dynamic-skill-trigger.d.ts +8 -0
  38. package/dist/src/dynamic-skill-trigger.js +62 -0
  39. package/dist/src/file-upload.d.ts +8 -0
  40. package/dist/src/file-upload.js +16 -8
  41. package/dist/src/formatter.js +21 -99
  42. package/dist/src/log-reporter/index.js +7 -7
  43. package/dist/src/monitor.js +0 -17
  44. package/dist/src/outbound.d.ts +0 -7
  45. package/dist/src/outbound.js +1 -59
  46. package/dist/src/provider.js +307 -56
  47. package/dist/src/reply-dispatcher.d.ts +0 -2
  48. package/dist/src/reply-dispatcher.js +97 -81
  49. package/dist/src/self-evolution-handler.d.ts +1 -1
  50. package/dist/src/skill-retriever/config.js +2 -2
  51. package/dist/src/skill-retriever/hooks.js +34 -13
  52. package/dist/src/skill-retriever/skill-status.d.ts +22 -0
  53. package/dist/src/skill-retriever/skill-status.js +125 -0
  54. package/dist/src/skill-retriever/tool-search.d.ts +4 -2
  55. package/dist/src/skill-retriever/tool-search.js +76 -16
  56. package/dist/src/skill-retriever/types.d.ts +1 -0
  57. package/dist/src/step-progress.d.ts +18 -0
  58. package/dist/src/step-progress.js +153 -0
  59. package/dist/src/tool-status-hook.d.ts +6 -0
  60. package/dist/src/tool-status-hook.js +79 -0
  61. package/dist/src/tools/agent-as-skill-tool.js +1 -1
  62. package/dist/src/tools/calendar-tool.js +1 -1
  63. package/dist/src/tools/call-device-tool.js +1 -1
  64. package/dist/src/tools/call-phone-tool.js +4 -2
  65. package/dist/src/tools/check-plugin-privilege-tool.js +1 -1
  66. package/dist/src/tools/create-alarm-tool.js +1 -1
  67. package/dist/src/tools/delete-alarm-tool.js +1 -1
  68. package/dist/src/tools/device-tool-map.d.ts +1 -1
  69. package/dist/src/tools/device-tool-map.js +30 -1
  70. package/dist/src/tools/discover-cross-devices-tool.js +1 -1
  71. package/dist/src/tools/display-a2ui-card-bypath-tool.js +43 -10
  72. package/dist/src/tools/hmos-cli.js +2 -2
  73. package/dist/src/tools/image-reading-tool.js +1 -1
  74. package/dist/src/tools/invoke.js +14 -6
  75. package/dist/src/tools/location-tool.js +1 -1
  76. package/dist/src/tools/login-token-tool.js +1 -1
  77. package/dist/src/tools/modify-alarm-tool.js +1 -1
  78. package/dist/src/tools/modify-note-tool.js +1 -1
  79. package/dist/src/tools/note-tool.js +1 -1
  80. package/dist/src/tools/query-app-message-tool.js +1 -1
  81. package/dist/src/tools/query-memory-data-tool.js +1 -1
  82. package/dist/src/tools/query-todo-task-tool.js +1 -1
  83. package/dist/src/tools/save-file-to-phone-tool.js +1 -1
  84. package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
  85. package/dist/src/tools/search-alarm-tool.js +1 -1
  86. package/dist/src/tools/search-calendar-tool.js +1 -1
  87. package/dist/src/tools/search-contact-tool.js +1 -1
  88. package/dist/src/tools/search-email-tool.js +1 -1
  89. package/dist/src/tools/search-file-tool.js +1 -1
  90. package/dist/src/tools/search-message-tool.js +1 -1
  91. package/dist/src/tools/search-note-tool.js +1 -1
  92. package/dist/src/tools/search-photo-gallery-tool.js +1 -1
  93. package/dist/src/tools/send-cross-device-task-tool.js +1 -1
  94. package/dist/src/tools/send-email-tool.js +1 -2
  95. package/dist/src/tools/send-file-to-user-tool.js +7 -6
  96. package/dist/src/tools/send-message-tool.js +4 -2
  97. package/dist/src/tools/session-manager.d.ts +4 -4
  98. package/dist/src/tools/session-manager.js +23 -12
  99. package/dist/src/tools/step-info-tool.d.ts +6 -0
  100. package/dist/src/tools/step-info-tool.js +352 -0
  101. package/dist/src/tools/upload-file-tool.js +1 -1
  102. package/dist/src/tools/upload-photo-tool.js +1 -1
  103. package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
  104. package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
  105. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
  106. package/dist/src/tools/xiaoyi-gui-tool.js +3 -3
  107. package/dist/src/transport/client.d.ts +36 -0
  108. package/dist/src/transport/client.js +120 -0
  109. package/dist/src/transport/heartbeat.d.ts +39 -0
  110. package/dist/src/transport/heartbeat.js +99 -0
  111. package/dist/src/transport/message-queue.d.ts +17 -0
  112. package/dist/src/transport/message-queue.js +52 -0
  113. package/dist/src/transport/push.d.ts +42 -0
  114. package/dist/src/transport/push.js +226 -0
  115. package/dist/src/transport/websocket.d.ts +124 -0
  116. package/dist/src/transport/websocket.js +839 -0
  117. package/dist/src/trigger-handler.js +1 -1
  118. package/dist/src/types.d.ts +8 -0
  119. package/dist/src/utils/async-mutex.d.ts +5 -0
  120. package/dist/src/utils/async-mutex.js +14 -0
  121. package/dist/src/utils/cron-push-map.js +29 -21
  122. package/dist/src/utils/pushdata-manager.d.ts +9 -1
  123. package/dist/src/utils/pushdata-manager.js +28 -19
  124. package/dist/src/utils/skill-path.d.ts +19 -0
  125. package/dist/src/utils/skill-path.js +34 -0
  126. package/dist/src/websocket.js +0 -1
  127. package/package.json +72 -72
package/dist/index.js CHANGED
@@ -15,7 +15,8 @@ import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
15
15
  import { registerCLIHook } from "./src/tools/hmos-cli.js";
16
16
  import { recoverCronState } from "./src/cron-recovery.js";
17
17
  import { writeSkillUsage } from "./src/utils/skills-logger.js";
18
- import { markSubagentSpawned, markSubagentEnded, getCachedXYConfig, } from "./src/subagent-wait-state.js";
18
+ import { markSubagentSpawned, markSubagentEnded, deliverSubagentFinalResult, } from "./src/conversation/conversation-manager.js";
19
+ import { notifyCronAgentEnd } from "./src/conversation/cron-buffer.js";
19
20
  import { logger } from "./src/utils/logger.js";
20
21
  /**
21
22
  * Parse a file path string to detect if it refers to a SKILL.md file within
@@ -44,23 +45,37 @@ function extractSkillNameFromPath(filePath) {
44
45
  * the `after_tool_call` hook and write the skill name to the skills log.
45
46
  */
46
47
  function registerSkillsDiagnosticHook(api) {
47
- // Tool name → skill name mapping for direct tool-based skill usage logging
48
- const TOOL_SKILL_MAP = {
49
- get_user_location: "GetCurrentLocation",
50
- get_calendar_tool_schema: "Schedule",
51
- get_note_tool_schema: "memorandum",
52
- get_photo_tool_schema: "gallery",
53
- get_contact_tool_schema: "contact",
54
- get_device_file_tool_schema: "file",
55
- get_alarm_tool_schema: "clock",
56
- message: "message",
57
- get_phone_tool_schema: "phone",
58
- get_collection_tool_schema: "xiaoyi-collection",
59
- image_reading: "xiaoyi-image-understanding"
48
+ // Skill name → tool names mapping for direct tool-based skill usage logging
49
+ const SKILL_TOOLS = {
50
+ GetCurrentLocation: ["get_user_location"],
51
+ Schedule: ["create_calendar_event", "search_calendar_event"],
52
+ memorandum: ["create_note", "search_notes", "modify_note"],
53
+ gallery: ["search_photo_gallery", "save_media_to_gallery"],
54
+ contact: ["search_contact"],
55
+ file: ["search_file", "upload_file", "save_file_to_file_manager"],
56
+ clock: ["create_alarm", "delete_alarm", "search_alarm", "modify_alarm"],
57
+ message: ["search_message", "send_message"],
58
+ phone: ["call_phone"],
59
+ "xiaoyi-collection": ["add_collection", "query_collection", "delete_collection"],
60
+ "xiaoyi-image-understanding": ["image_reading"],
60
61
  };
62
+ // Inverted to tool name → skill name for lookup
63
+ const TOOL_SKILL_MAP = Object.fromEntries(Object.entries(SKILL_TOOLS).flatMap(([skill, tools]) => tools.map((t) => [t, skill])));
64
+ // Resolve the actual tool name from call_device_tool wrapper.
65
+ // The model calls call_device_tool({ toolName: "...", arguments: {...} })
66
+ // — the real tool name is inside params, not event.toolName.
67
+ function resolveActualToolName(event) {
68
+ if (event.toolName === "call_device_tool") {
69
+ const inner = event.params?.toolName;
70
+ if (typeof inner === "string" && inner.length > 0)
71
+ return inner;
72
+ }
73
+ return event.toolName;
74
+ }
61
75
  // Log skill usage for known device tools on before_tool_call
62
76
  api.on("before_tool_call", async (event, _ctx) => {
63
- const skillName = TOOL_SKILL_MAP[event.toolName];
77
+ const actualToolName = resolveActualToolName(event);
78
+ const skillName = TOOL_SKILL_MAP[actualToolName];
64
79
  if (skillName) {
65
80
  writeSkillUsage(skillName);
66
81
  }
@@ -75,19 +90,32 @@ function registerSkillsDiagnosticHook(api) {
75
90
  }
76
91
  });
77
92
  }
93
+ /**
94
+ * 从 openclaw sessionKey 解析 cron jobId。
95
+ * 格式形如 `agent:<agentId>:cron:<jobId>:run:<startedAtTs>`(隔离 run 为
96
+ * `cron:<jobId>`),jobId 位于 "cron:" 之后到下一个冒号/空白之前。
97
+ */
98
+ function parseCronJobIdFromSessionKey(sessionKey) {
99
+ const match = sessionKey.match(/cron:([^:\s]+)/);
100
+ return match ? match[1] : undefined;
101
+ }
78
102
  /**
79
103
  * Register the cron detection hook.
80
104
  *
81
105
  * When openclaw's cron runner triggers a tool call, the sessionKey has the
82
- * format "cron:<jobId>". We use this to mark the toolCallId in a global Map
106
+ * format "cron:<jobId>"(或 agent 展开后的 `agent:<id>:cron:<jobId>:run:<ts>`)。
107
+ * We use this to mark the toolCallId in a global Map
83
108
  * so that sendCommand() can route the command through the push channel
84
109
  * instead of the (non-existent) WebSocket session.
85
110
  */
86
111
  function registerCronDetectionHook(api) {
87
112
  api.on("before_tool_call", async (event, ctx) => {
88
- // sessionKey 前缀依赖 openclaw 版本行为,不一定可靠;
89
- // isCronActive() 由 provider.ts 根据消息内容 [cron:...] 设置,更可靠。
90
- if ((ctx.sessionKey?.startsWith("cron:") || isCronActive()) && event.toolCallId) {
113
+ // sessionKey 前缀依赖 openclaw 版本行为,不一定可靠;isCronActive(jobId)
114
+ // 按 jobId 精确判断(provider 已从消息 [cron:...] 标记过该 job),替代
115
+ // 原先的全局 120s 时间戳,避免把普通对话/其它 job 的工具调用误判为 cron。
116
+ const sessionKey = ctx.sessionKey ?? "";
117
+ const sessionJobId = parseCronJobIdFromSessionKey(sessionKey);
118
+ if ((sessionKey.includes("cron:") || isCronActive(sessionJobId)) && event.toolCallId) {
91
119
  markCronToolCall(event.toolCallId);
92
120
  // 存储 runId,供 call_device_tool 在 ALS 缺失时构造合成 SessionContext
93
121
  if (event.runId) {
@@ -305,6 +333,53 @@ function registerCronRecoveryHook(api) {
305
333
  logger.log(`${logTag} ═══════════════════════════════════════════`);
306
334
  });
307
335
  }
336
+ /**
337
+ * 从 sessions_spawn 工具结果中提取 ACP child sessionKey。
338
+ * 兼容三种 result 形态:{status, childSessionKey} 直返、JSON 字符串、
339
+ * {content:[{text: "<json>"}], details:{...}} 工具信封。
340
+ * 仅接受 status="accepted" 且 key 形如 agent:<id>:acp:<uuid> 的结果。
341
+ */
342
+ function extractAcceptedAcpChildSessionKey(result) {
343
+ const pick = (obj) => {
344
+ if (!obj || typeof obj !== "object")
345
+ return null;
346
+ const record = obj;
347
+ if (record.status !== "accepted")
348
+ return null;
349
+ const key = record.childSessionKey;
350
+ return typeof key === "string" && key.includes(":acp:") ? key : null;
351
+ };
352
+ const direct = pick(result);
353
+ if (direct)
354
+ return direct;
355
+ const envelope = result;
356
+ const fromDetails = pick(envelope?.details);
357
+ if (fromDetails)
358
+ return fromDetails;
359
+ if (Array.isArray(envelope?.content)) {
360
+ for (const item of envelope.content) {
361
+ if (typeof item?.text !== "string")
362
+ continue;
363
+ try {
364
+ const fromText = pick(JSON.parse(item.text));
365
+ if (fromText)
366
+ return fromText;
367
+ }
368
+ catch {
369
+ // 非 JSON 文本,忽略
370
+ }
371
+ }
372
+ }
373
+ if (typeof result === "string") {
374
+ try {
375
+ return pick(JSON.parse(result));
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ return null;
382
+ }
308
383
  function registerSubagentHooks(api) {
309
384
  // subagent_spawned: fires after a subagent run is successfully registered.
310
385
  // We increment the expected completion count so that onIdle knows to wait.
@@ -317,6 +392,32 @@ function registerSubagentHooks(api) {
317
392
  logger.log(`[XY-SUBAGENT] spawned, requesterSessionKey=${requesterSessionKey.slice(0, 30)}, expected=${count}`);
318
393
  }
319
394
  });
395
+ // ACP SPAWN TRACKING: openclaw 的 subagent_spawned hook 仅由内嵌 subagent
396
+ // 路径(subagent-spawn.ts)发射;acp-spawn.ts(runtime="acp",如 Claude Code)
397
+ // 不发射 —— 实测 ACP run 仅在结束时经 run registry 触发 subagent_ended,
398
+ // 导致等待态从未建立、父 turn onIdle 直接发 final 结束对话。
399
+ // 因此在 after_tool_call 补记:sessions_spawn 成功且 childSessionKey 为 ACP
400
+ // 形态时递增等待计数。内嵌 subagent 的 childKey 形如 agent:<id>:subagent:<id>,
401
+ // 不命中此分支,仍由 subagent_spawned hook 负责,不会重复计数。
402
+ api.on("after_tool_call", async (event, ctx) => {
403
+ try {
404
+ if (event?.toolName !== "sessions_spawn" || event?.error)
405
+ return;
406
+ const childKey = extractAcceptedAcpChildSessionKey(event?.result);
407
+ if (!childKey)
408
+ return;
409
+ const sessionKey = ctx?.sessionKey;
410
+ if (!sessionKey)
411
+ return;
412
+ const count = markSubagentSpawned(sessionKey);
413
+ if (count > 0) {
414
+ logger.log(`[XY-SUBAGENT] ACP spawned, childSessionKey=${childKey.slice(0, 40)}, expected=${count}`);
415
+ }
416
+ }
417
+ catch (err) {
418
+ logger.error(`[XY-SUBAGENT] Error tracking ACP spawn:`, err);
419
+ }
420
+ });
320
421
  // subagent_ended: fires when a subagent run terminates (complete/error/killed).
321
422
  // This is the PRIMARY delivery tracking mechanism. When all expected
322
423
  // subagents have ended and parent has settled, we finalize the A2A session.
@@ -331,15 +432,13 @@ function registerSubagentHooks(api) {
331
432
  logger.log(`[XY-SUBAGENT-END] ended, targetSessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome}, complete=${transition?.isComplete ?? false}, shouldFinalize=${transition?.shouldFinalize ?? false}, transition=${!!transition}`);
332
433
  if (transition?.shouldFinalize) {
333
434
  logger.log(`[XY-SUBAGENT-END] Starting finalization...`);
334
- const config = getCachedXYConfig();
335
- if (!config) {
336
- logger.error(`[XY-SUBAGENT-END] No cached XY config, cannot deliver final result`);
337
- return;
338
- }
339
- const { deliverSubagentFinalResult } = await import("./src/outbound.js");
340
- logger.log(`[XY-SUBAGENT-END] Using cached XY config`);
435
+ // Grace: announce 投递的 sendText 与 subagent_ended hook 并发到达
436
+ // (实测 sendText 可能先于 hook 约 1s,也可能晚于 hook;长文本还会
437
+ // 被 textChunkLimit 分片成多次 sendText)。捕获窗口直到
438
+ // finalDeliveryStarted 才关闭,此处的等待直接扩大迟到来片的入窗机会。
439
+ await new Promise((r) => setTimeout(r, 3000));
440
+ // 最终交付由对话管理层统一负责(含 config 解析、final 帧、清理)
341
441
  await deliverSubagentFinalResult({
342
- config: config,
343
442
  state: transition.state,
344
443
  reason: "all-subagents-ended-after-parent-settled",
345
444
  });
@@ -354,14 +453,45 @@ function registerSubagentHooks(api) {
354
453
  function registerFullHooks(api) {
355
454
  // SUBAGENT HOOKS: track subagent spawn/end lifecycle for session keep-alive
356
455
  registerSubagentHooks(api);
456
+ // CRON AGGREGATION: 通过 agent 事件总线感知 cron isolated run 结束。
457
+ // 注意:agent_end 插件 hook 只在 dispatch-from-config 路径触发,cron
458
+ // isolated run 不会触发它;embedded run 的 lifecycle 事件才是可靠信号。
459
+ // phase=end/error 后到达的 sendText 即为 announce 最终结果(放行)。
460
+ api.agent.events.registerAgentEventSubscription({
461
+ id: "xy-cron-turn-tracker",
462
+ description: "Track cron isolated run completion for sendText aggregation",
463
+ streams: ["lifecycle"],
464
+ handle: (event) => {
465
+ try {
466
+ const phase = event?.data?.phase;
467
+ // 仅 phase=end 是 run 级终态信号;phase=error 可能是单次模型调用
468
+ // 失败(后续还有重试/start),不能据此放行 announce。
469
+ // 真正失败的 run(无 end)由 120s 安全超时兜底 flush 缓冲文本。
470
+ if (phase !== "end")
471
+ return;
472
+ const sessionKey = event?.sessionKey ?? "";
473
+ // 从 sessionKey(agent:<id>:cron:<jobId>:run:<ts>)解析 jobId,只结束
474
+ // 该 job 的 turn;isCronActive(jobId) 作为无 cron 前缀版本的兜底(按
475
+ // jobId 精确判断,普通对话的 lifecycle end 不会命中)。
476
+ const cronJobId = parseCronJobIdFromSessionKey(sessionKey);
477
+ if (sessionKey.includes("cron:") || sessionKey.startsWith("cron:") || isCronActive(cronJobId)) {
478
+ logger.log(`[XY-CRON] Cron run lifecycle end, sessionKey=${sessionKey || "unknown"}, jobId=${cronJobId ?? "-"}`);
479
+ notifyCronAgentEnd(cronJobId);
480
+ }
481
+ }
482
+ catch (err) {
483
+ logger.error(`[XY-CRON] Error in cron lifecycle subscription:`, err);
484
+ }
485
+ },
486
+ });
357
487
  // SKILL RETRIEVER HOOK: before_prompt_build hook
358
488
  const pluginConfig = api.pluginConfig || {};
359
489
  const skillRetrieverConfig = normalizeToolRetrieverConfig({
360
490
  enabled: pluginConfig.skillRetrieverEnabled ?? true,
361
- maxTools: pluginConfig.skillRetrieverMaxTools ?? 2,
491
+ maxTools: pluginConfig.skillRetrieverMaxTools ?? 6,
362
492
  includeUninstalledOnly: true,
363
493
  envFilePath: "~/.openclaw/.xiaoyienv",
364
- timeoutMs: pluginConfig.skillRetrieverTimeoutMs ?? 1000,
494
+ timeoutMs: pluginConfig.skillRetrieverTimeoutMs ?? 10000,
365
495
  });
366
496
  const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
367
497
  api.on("before_prompt_build", async (event, ctx) => {
package/dist/src/bot.js CHANGED
@@ -17,7 +17,6 @@ import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
17
17
  import { setCsplSteerContext } from "./cspl/steer-context.js";
18
18
  import { resolveActiveEmbeddedRunSessionId, queueAgentHarnessMessage, } from "openclaw/plugin-sdk/agent-harness-runtime";
19
19
  import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
20
- import { registerSessionKeyMapping, getWaitState, hasWaitState, markParentSettled, cacheXYConfig, } from "./subagent-wait-state.js";
21
20
  import { logger } from "./utils/logger.js";
22
21
  /**
23
22
  * Handle an incoming A2A message.
@@ -166,7 +165,6 @@ export async function handleXYMessage(params) {
166
165
  const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
167
166
  // Resolve configuration (needed for status updates)
168
167
  const config = resolveXYConfig(cfg);
169
- cacheXYConfig(config);
170
168
  // ✅ Resolve agent route (following feishu pattern)
171
169
  // accountId is "default" for XY (single account mode)
172
170
  // Use sessionId as peer.id to ensure all messages in the same session share context
@@ -180,16 +178,6 @@ export async function handleXYMessage(params) {
180
178
  },
181
179
  });
182
180
  log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
183
- // Register sessionKey→A2A sessionId mapping for subagent hook translation.
184
- // Hooks receive openclaw sessionKey (e.g. "agent:main:direct:xxx") but
185
- // xy_channel operates on A2A sessionId. This bridge lets subagent_spawned/
186
- // subagent_ended hooks find the correct wait state.
187
- // IMPORTANT: Only register on the FIRST message. Steer messages have a
188
- // different taskId — overwriting the mapping would break subagent wait
189
- // state tracking for the original message's subagents.
190
- if (!isUpdate) {
191
- registerSessionKeyMapping(route.sessionKey, parsed.sessionId, parsed.taskId, parsed.messageId);
192
- }
193
181
  // Check for ACP runtime binding on this A2A conversation
194
182
  const runtimeRoute = resolveRuntimeConversationBindingRoute({
195
183
  route,
@@ -313,7 +301,7 @@ export async function handleXYMessage(params) {
313
301
  // per-session ReplyOperation lock in admitReplyTurn which would otherwise
314
302
  // block the steer message until the first message completes — making steer
315
303
  // indistinguishable from a followup.
316
- if (isUpdate && !skipReg && route.sessionKey) {
304
+ if (isUpdate && route.sessionKey) {
317
305
  const activeSessionId = resolveActiveEmbeddedRunSessionId(route.sessionKey);
318
306
  if (activeSessionId) {
319
307
  log.log(`[BOT-STEER] Direct steer attempt: activeSessionId=${activeSessionId}, textLen=${textForAgent.length}`);
@@ -397,15 +385,6 @@ export async function handleXYMessage(params) {
397
385
  cleaned = true;
398
386
  log.log(`[BOT] Cleanup started, steered=${steerState.steered}, skipReg=${skipReg}`);
399
387
  if (!skipReg) {
400
- // Check for pending subagent wait on this session
401
- const pendingWait = getWaitState(parsed.sessionId, parsed.taskId) ??
402
- (hasWaitState(parsed.sessionId) ? { deliveredCompletions: 0, expectedCompletions: 1 } : null);
403
- if (pendingWait && pendingWait.deliveredCompletions < pendingWait.expectedCompletions) {
404
- // Subagent wait active — skip cleanup, session stays alive
405
- log.log(`[BOT] Cleanup suppressed — subagent wait active on session, taskId=${parsed.taskId}`);
406
- cleaned = false;
407
- return;
408
- }
409
388
  decrementTaskIdRef(parsed.sessionId);
410
389
  }
411
390
  else {
@@ -454,27 +433,6 @@ export async function handleXYMessage(params) {
454
433
  log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
455
434
  return;
456
435
  }
457
- // Subagent wait state: if parent has pending subagents, mark settled
458
- // and defer finalization. Cleanup is suppressed so the session stays alive.
459
- const pendingWait = getWaitState(parsed.sessionId, parsed.taskId);
460
- if (pendingWait && !pendingWait.parentSettled) {
461
- const transition = markParentSettled(parsed.sessionId, parsed.taskId);
462
- if (transition?.shouldFinalize) {
463
- // All completions arrived before parent settled → finalize now
464
- const { deliverSubagentFinalResult } = await import("./outbound.js");
465
- await deliverSubagentFinalResult({
466
- config,
467
- state: transition.state,
468
- reason: "all-subagent-results-delivered-before-parent-settled",
469
- });
470
- log.log(`[BOT] Subagent wait complete on parent settled; final response delivered`);
471
- }
472
- else {
473
- log.log(`[BOT] Subagent wait active, preserving session context for completion`);
474
- }
475
- dispatcherUpdaters.delete(parsed.sessionId);
476
- return;
477
- }
478
436
  // cleanup 已由 onIdleComplete 在 onIdle 的 finally 中执行。
479
437
  // onSettled 不做任何清理(直接在这里清理会发生 race condition)。
480
438
  dispatcherUpdaters.delete(parsed.sessionId);
@@ -486,7 +444,7 @@ export async function handleXYMessage(params) {
486
444
  // signal init complete to release the global dispatch gate
487
445
  // for the next session.
488
446
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
489
- const isSteerDispatch = isUpdate && !skipReg;
447
+ const isSteerDispatch = isUpdate;
490
448
  log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=${isSteerDispatch}`);
491
449
  log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
492
450
  try {
@@ -1,6 +1,6 @@
1
1
  import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
2
2
  import { xyConfigSchema } from "./config-schema.js";
3
- import { xyOutbound } from "./outbound.js";
3
+ import { xyOutbound } from "./dispatch/outbound.js";
4
4
  import { filterToolsByDevice } from "./tools/device-tool-map.js";
5
5
  import { getCurrentSessionContext } from "./tools/session-manager.js";
6
6
  import { logger } from "./utils/logger.js";
@@ -23,16 +23,14 @@ import { getAlarmToolSchemaTool } from "./tools/get-alarm-tool-schema.js";
23
23
  import { getCollectionToolSchemaTool } from "./tools/get-collection-tool-schema.js";
24
24
  import { loginTokenTool } from "./tools/login-token-tool.js";
25
25
  import { agentAsSkillTool } from "./tools/agent-as-skill-tool.js";
26
- import { discoverCrossDevicesTool } from "./tools/discover-cross-devices-tool.js";
27
- import { sendCrossDeviceTaskTool } from "./tools/send-cross-device-task-tool.js";
26
+ // import { discoverCrossDevicesTool } from "./tools/discover-cross-devices-tool.js";
27
+ // import { sendCrossDeviceTaskTool } from "./tools/send-cross-device-task-tool.js";
28
28
  import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
29
- import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
29
+ // import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
30
30
  import { invokeTool } from "./tools/invoke.js";
31
31
  import { xiaoyiAppendReferenceTool } from "./tools/xiaoyi-append-reference.js";
32
32
  const ALL_TOOLS = [
33
33
  locationTool,
34
- discoverCrossDevicesTool,
35
- sendCrossDeviceTaskTool,
36
34
  displayA2UICardByPathTool,
37
35
  callDeviceTool,
38
36
  getNoteToolSchemaTool,
@@ -51,7 +49,6 @@ const ALL_TOOLS = [
51
49
  saveSelfEvolutionSkillTool,
52
50
  loginTokenTool,
53
51
  agentAsSkillTool,
54
- checkPluginPrivilegeTool,
55
52
  invokeTool,
56
53
  xiaoyiAppendReferenceTool,
57
54
  ];
@@ -158,7 +155,7 @@ export const xyPlugin = {
158
155
  },
159
156
  gateway: {
160
157
  async startAccount(context) {
161
- const { monitorXYProvider } = await import("./monitor.js");
158
+ const { monitorXYProvider } = await import("./dispatch/monitor.js");
162
159
  const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
163
160
  const account = resolveXYConfig(context.cfg);
164
161
  context.setStatus?.({
@@ -49,6 +49,19 @@ export declare const xyConfigSchema: {
49
49
  readonly type: "string";
50
50
  readonly description: "Default session ID for push notifications (used when no target is specified, e.g., in cron jobs)";
51
51
  };
52
+ readonly terminalFrameDelayMs: {
53
+ readonly type: "number";
54
+ readonly description: "Delay in ms before sending terminal frames (completed status + final) so the content artifact clears downstream pipelines first; 0 disables";
55
+ readonly default: 300;
56
+ };
57
+ readonly pinnedSessionIds: {
58
+ readonly type: "array";
59
+ readonly items: {
60
+ readonly type: "string";
61
+ };
62
+ readonly description: "A2A sessionIds that never change (e.g. [\"00000000\"]). For these, clearContext triggers an explicit openclaw session reset (sessions.reset) to isolate context. Set to [] to disable.";
63
+ readonly default: readonly ["00000000"];
64
+ };
52
65
  };
53
66
  readonly required: readonly ["apiKey", "agentId", "uid", "apiId", "pushId"];
54
67
  };
@@ -50,6 +50,17 @@ export const xyConfigSchema = {
50
50
  type: "string",
51
51
  description: "Default session ID for push notifications (used when no target is specified, e.g., in cron jobs)",
52
52
  },
53
+ terminalFrameDelayMs: {
54
+ type: "number",
55
+ description: "Delay in ms before sending terminal frames (completed status + final) so the content artifact clears downstream pipelines first; 0 disables",
56
+ default: 300,
57
+ },
58
+ pinnedSessionIds: {
59
+ type: "array",
60
+ items: { type: "string" },
61
+ description: "A2A sessionIds that never change (e.g. [\"00000000\"]). For these, clearContext triggers an explicit openclaw session reset (sessions.reset) to isolate context. Set to [] to disable.",
62
+ default: ["00000000"],
63
+ },
53
64
  },
54
65
  required: ["apiKey", "agentId", "uid", "apiId", "pushId"],
55
66
  };
@@ -27,6 +27,11 @@ export function resolveXYConfig(cfg) {
27
27
  fileUploadUrl: xyConfig.fileUploadUrl ?? "http://localhost:8767",
28
28
  pushUrl: xyConfig.pushUrl,
29
29
  defaultSessionId: xyConfig.defaultSessionId,
30
+ terminalFrameDelayMs: xyConfig.terminalFrameDelayMs ?? 300,
31
+ // 固定会话列表:缺省默认 ["00000000"];显式配置 [] 表示禁用 reset 特殊处理
32
+ pinnedSessionIds: Array.isArray(xyConfig.pinnedSessionIds)
33
+ ? xyConfig.pinnedSessionIds.filter((id) => typeof id === "string" && id.trim() !== "")
34
+ : ["00000000"],
30
35
  };
31
36
  }
32
37
  /**
@@ -0,0 +1,96 @@
1
+ import { ConversationSession, SubagentWaitState, SubagentWaitTransition, TaskEntry } from "./conversation-session.js";
2
+ export declare function getOrCreateSession(sessionId: string): ConversationSession;
3
+ export declare function getSession(sessionId: string): ConversationSession | null;
4
+ export declare function deleteSession(sessionId: string): void;
5
+ export declare function listSessions(): ConversationSession[];
6
+ /** 会话状态迁移(working / waiting-subagent / completing / 终态)。 */
7
+ export declare function setSessionState(sessionId: string, state: ConversationSession["state"]): void;
8
+ /**
9
+ * 注册或更新 session 的活跃 taskId。
10
+ * Returns true if this was an update (session already had an active task).
11
+ */
12
+ export declare function registerTask(sessionId: string, taskId: string, messageId: string): boolean;
13
+ /**
14
+ * 移除 session 的活跃 taskId(消息处理完成时调用)。
15
+ * @param expectedTaskId 可选:精确移除指定的 taskId,而非清空整个任务链。
16
+ */
17
+ export declare function completeTask(sessionId: string, expectedTaskId?: string): void;
18
+ /** 获取 session 当前活跃 task(任务链末尾)。 */
19
+ export declare function getCurrentTask(sessionId: string): TaskEntry | null;
20
+ /** 获取 session 当前活跃 taskId(reply-dispatcher 跨链读取 / 工具富化用)。 */
21
+ export declare function getCurrentTaskId(sessionId: string): string | null;
22
+ /** 获取 session 当前活跃 messageId。 */
23
+ export declare function getCurrentMessageId(sessionId: string): string | null;
24
+ export declare function hasActiveTask(sessionId: string): boolean;
25
+ /** 所有活跃 task 绑定(gateway_stop 通知等场景)。 */
26
+ export declare function listActiveTaskBindings(): Array<{
27
+ sessionId: string;
28
+ currentTaskId: string;
29
+ currentMessageId: string;
30
+ updatedAt: number;
31
+ }>;
32
+ /**
33
+ * dispatch 时登记在途 turn(bot.ts 创建 dispatcher 后调用)。
34
+ * 注意:steer 直接注入成功的路径不 dispatch,不得登记。
35
+ */
36
+ export declare function markTurnInflight(sessionId: string, taskId: string): void;
37
+ /** turn 不再产生流式帧时清除在途标记(onIdle 入口 / onSettled 兜底)。 */
38
+ export declare function clearTurnInflight(sessionId: string, taskId: string): void;
39
+ /**
40
+ * 绑定 openclaw sessionKey 到首条消息的 A2A 身份。
41
+ * 仅在首条消息时调用(steer 消息不覆盖,保持 subagent 等待态跟踪稳定)。
42
+ */
43
+ export declare function bindSessionKey(sessionKey: string, sessionId: string, taskId: string, messageId: string): void;
44
+ export declare function resolveBySessionKey(sessionKey: string): {
45
+ sessionId: string;
46
+ taskId: string;
47
+ messageId: string;
48
+ } | null;
49
+ export declare function unbindSessionKey(sessionKey: string): void;
50
+ /**
51
+ * 全局缓存 resolved XY config,供 hook 上下文(无 raw ClawdbotConfig)使用。
52
+ */
53
+ export declare function cacheXYConfig(config: unknown): void;
54
+ export declare function getCachedXYConfig(): unknown;
55
+ /**
56
+ * 启动(或重启)session 的 30s 状态心跳。
57
+ * 心跳由 manager 拥有:dispatcher 销毁不影响心跳;
58
+ * tick 时读取任务链末尾的当前 taskId/messageId。
59
+ */
60
+ export declare function startStatusHeartbeat(sessionId: string, text?: string): void;
61
+ export declare function stopStatusHeartbeat(sessionId: string): void;
62
+ /**
63
+ * subagent_spawned hook 调用。递增 expected completion 计数。
64
+ * 若等待态尚不存在则创建(sessions_spawn 可能先于 sessions_yield)。
65
+ */
66
+ export declare function markSubagentSpawned(sessionKey: string): number;
67
+ /**
68
+ * subagent_ended hook 调用。标记一个 subagent run 结束并计为一次交付。
69
+ * 这是主要的完成跟踪机制(subagent 完成可能不经 xyOutbound.sendText)。
70
+ */
71
+ export declare function markSubagentEnded(sessionKey: string): SubagentWaitTransition | null;
72
+ /**
73
+ * bot.ts onSettled 调用。标记父 dispatcher 已 settle。
74
+ */
75
+ export declare function markParentSettled(sessionId: string, taskId: string): SubagentWaitTransition | null;
76
+ /**
77
+ * 存储从 xyOutbound.sendText 捕获的完成文本。不递增交付计数。
78
+ */
79
+ export declare function addCompletionText(sessionId: string, taskId: string, text: string): void;
80
+ export declare function getWaitState(sessionId: string, taskId?: string): SubagentWaitState | null;
81
+ export declare function hasWaitState(sessionId: string, taskId?: string): boolean;
82
+ export declare function clearWaitState(sessionId: string, reason: string, taskId?: string): SubagentWaitState | null;
83
+ /**
84
+ * 当所有 subagent 完成且父 turn 已 settle 时,由 manager 发送终态帧并真正
85
+ * 结束当前 A2A session:
86
+ * 1. completed 状态帧("任务处理已完成~")→ final:true 全文本帧(合并
87
+ * completionTexts)——与 reply-dispatcher 正常终态同序,xy_tester
88
+ * 与客户端依赖该顺序
89
+ * 2. 清理等待态、停止心跳、移除任务链
90
+ * 3. 会话状态迁移到 completed
91
+ */
92
+ export declare function deliverSubagentFinalResult(params: {
93
+ state: SubagentWaitState;
94
+ reason?: string;
95
+ text?: string;
96
+ }): Promise<void>;