@ynhcj/xiaoyi-channel 0.0.222-next → 0.0.223-next

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 (103) hide show
  1. package/dist/index.js +31 -9
  2. package/dist/src/bot.js +2 -2
  3. package/dist/src/channel.js +2 -2
  4. package/dist/src/conversation/conversation-manager.d.ts +87 -0
  5. package/dist/src/conversation/conversation-manager.js +387 -0
  6. package/dist/src/conversation/conversation-session.d.ts +38 -0
  7. package/dist/src/conversation/conversation-session.js +17 -0
  8. package/dist/src/conversation/cron-buffer.d.ts +25 -0
  9. package/dist/src/conversation/cron-buffer.js +168 -0
  10. package/dist/src/conversation/outbound-gateway.d.ts +39 -0
  11. package/dist/src/conversation/outbound-gateway.js +81 -0
  12. package/dist/src/conversation/steer-service.d.ts +29 -0
  13. package/dist/src/conversation/steer-service.js +52 -0
  14. package/dist/src/cron-command.js +2 -24
  15. package/dist/src/cspl/configs.json +10 -10
  16. package/dist/src/cspl/sentinel_hook.js +7 -10
  17. package/dist/src/dispatch/bot.d.ts +20 -0
  18. package/dist/src/dispatch/bot.js +510 -0
  19. package/dist/src/dispatch/monitor.d.ts +17 -0
  20. package/dist/src/dispatch/monitor.js +333 -0
  21. package/dist/src/dispatch/outbound.d.ts +7 -0
  22. package/dist/src/dispatch/outbound.js +251 -0
  23. package/dist/src/dispatch/reply-dispatcher.d.ts +22 -0
  24. package/dist/src/dispatch/reply-dispatcher.js +505 -0
  25. package/dist/src/formatter.js +13 -91
  26. package/dist/src/monitor.js +0 -17
  27. package/dist/src/provider.js +4 -0
  28. package/dist/src/self-evolution-handler.d.ts +1 -1
  29. package/dist/src/tools/agent-as-skill-tool.js +1 -1
  30. package/dist/src/tools/calendar-tool.js +1 -1
  31. package/dist/src/tools/call-device-tool.js +1 -1
  32. package/dist/src/tools/call-phone-tool.js +1 -1
  33. package/dist/src/tools/check-plugin-privilege-tool.js +1 -1
  34. package/dist/src/tools/create-alarm-tool.js +1 -1
  35. package/dist/src/tools/delete-alarm-tool.js +1 -1
  36. package/dist/src/tools/discover-cross-devices-tool.js +1 -1
  37. package/dist/src/tools/hmos-cli.js +2 -2
  38. package/dist/src/tools/invoke.js +2 -2
  39. package/dist/src/tools/location-tool.js +1 -1
  40. package/dist/src/tools/login-token-tool.js +1 -1
  41. package/dist/src/tools/modify-alarm-tool.js +1 -1
  42. package/dist/src/tools/modify-note-tool.js +1 -1
  43. package/dist/src/tools/note-tool.js +1 -1
  44. package/dist/src/tools/query-app-message-tool.js +1 -1
  45. package/dist/src/tools/query-memory-data-tool.js +1 -1
  46. package/dist/src/tools/query-todo-task-tool.js +1 -1
  47. package/dist/src/tools/save-file-to-phone-tool.js +1 -1
  48. package/dist/src/tools/save-media-to-gallery-tool.js +1 -1
  49. package/dist/src/tools/search-alarm-tool.js +1 -1
  50. package/dist/src/tools/search-calendar-tool.js +1 -1
  51. package/dist/src/tools/search-contact-tool.js +1 -1
  52. package/dist/src/tools/search-email-tool.js +1 -1
  53. package/dist/src/tools/search-file-tool.js +1 -1
  54. package/dist/src/tools/search-message-tool.js +1 -1
  55. package/dist/src/tools/search-note-tool.js +1 -1
  56. package/dist/src/tools/search-photo-gallery-tool.js +1 -1
  57. package/dist/src/tools/send-cross-device-task-tool.js +1 -1
  58. package/dist/src/tools/send-email-tool.js +1 -1
  59. package/dist/src/tools/send-file-to-user-tool.js +1 -1
  60. package/dist/src/tools/send-message-tool.js +1 -1
  61. package/dist/src/tools/session-manager.js +1 -1
  62. package/dist/src/tools/upload-file-tool.js +1 -1
  63. package/dist/src/tools/upload-photo-tool.js +1 -1
  64. package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -1
  65. package/dist/src/tools/xiaoyi-collection-tool.js +1 -1
  66. package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
  67. package/dist/src/tools/xiaoyi-gui-tool.js +3 -3
  68. package/dist/src/transport/client.d.ts +36 -0
  69. package/dist/src/transport/client.js +120 -0
  70. package/dist/src/transport/heartbeat.d.ts +39 -0
  71. package/dist/src/transport/heartbeat.js +99 -0
  72. package/dist/src/transport/message-queue.d.ts +17 -0
  73. package/dist/src/transport/message-queue.js +52 -0
  74. package/dist/src/transport/push.d.ts +40 -0
  75. package/dist/src/transport/push.js +218 -0
  76. package/dist/src/transport/websocket.d.ts +124 -0
  77. package/dist/src/transport/websocket.js +839 -0
  78. package/dist/src/trigger-handler.js +1 -1
  79. package/package.json +2 -2
  80. package/dist/src/cspl/call-api.d.ts +0 -9
  81. package/dist/src/cspl/call-api.js +0 -110
  82. package/dist/src/cspl/middleware.d.ts +0 -8
  83. package/dist/src/cspl/middleware.js +0 -90
  84. package/dist/src/log-reporter/config-loader.d.ts +0 -11
  85. package/dist/src/log-reporter/config-loader.js +0 -68
  86. package/dist/src/steer-injector.d.ts +0 -16
  87. package/dist/src/steer-injector.js +0 -74
  88. package/dist/src/steer-queue.d.ts +0 -1
  89. package/dist/src/steer-queue.js +0 -9
  90. package/dist/src/thread-bindings.d.ts +0 -54
  91. package/dist/src/thread-bindings.js +0 -214
  92. package/dist/src/tools/create-all-tools.d.ts +0 -16
  93. package/dist/src/tools/create-all-tools.js +0 -62
  94. package/dist/src/tools/display-a2ui-card-tool.d.ts +0 -2
  95. package/dist/src/tools/display-a2ui-card-tool.js +0 -85
  96. package/dist/src/tools/find-pc-devices-tool.d.ts +0 -6
  97. package/dist/src/tools/find-pc-devices-tool.js +0 -95
  98. package/dist/src/tools/send-command-to-car-tool.d.ts +0 -5
  99. package/dist/src/tools/send-command-to-car-tool.js +0 -85
  100. package/dist/src/tools/session-helper.d.ts +0 -24
  101. package/dist/src/tools/session-helper.js +0 -45
  102. package/dist/src/xy-session-store.d.ts +0 -79
  103. package/dist/src/xy-session-store.js +0 -153
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
@@ -56,6 +57,7 @@ function registerSkillsDiagnosticHook(api) {
56
57
  message: "message",
57
58
  get_phone_tool_schema: "phone",
58
59
  get_collection_tool_schema: "xiaoyi-collection",
60
+ image_reading: "xiaoyi-image-understanding"
59
61
  };
60
62
  // Log skill usage for known device tools on before_tool_call
61
63
  api.on("before_tool_call", async (event, _ctx) => {
@@ -330,15 +332,8 @@ function registerSubagentHooks(api) {
330
332
  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}`);
331
333
  if (transition?.shouldFinalize) {
332
334
  logger.log(`[XY-SUBAGENT-END] Starting finalization...`);
333
- const config = getCachedXYConfig();
334
- if (!config) {
335
- logger.error(`[XY-SUBAGENT-END] No cached XY config, cannot deliver final result`);
336
- return;
337
- }
338
- const { deliverSubagentFinalResult } = await import("./src/outbound.js");
339
- logger.log(`[XY-SUBAGENT-END] Using cached XY config`);
335
+ // 最终交付由对话管理层统一负责(含 config 解析、final 帧、清理)
340
336
  await deliverSubagentFinalResult({
341
- config: config,
342
337
  state: transition.state,
343
338
  reason: "all-subagents-ended-after-parent-settled",
344
339
  });
@@ -353,6 +348,33 @@ function registerSubagentHooks(api) {
353
348
  function registerFullHooks(api) {
354
349
  // SUBAGENT HOOKS: track subagent spawn/end lifecycle for session keep-alive
355
350
  registerSubagentHooks(api);
351
+ // CRON AGGREGATION: 通过 agent 事件总线感知 cron isolated run 结束。
352
+ // 注意:agent_end 插件 hook 只在 dispatch-from-config 路径触发,cron
353
+ // isolated run 不会触发它;embedded run 的 lifecycle 事件才是可靠信号。
354
+ // phase=end/error 后到达的 sendText 即为 announce 最终结果(放行)。
355
+ api.agent.events.registerAgentEventSubscription({
356
+ id: "xy-cron-turn-tracker",
357
+ description: "Track cron isolated run completion for sendText aggregation",
358
+ streams: ["lifecycle"],
359
+ handle: (event) => {
360
+ try {
361
+ const phase = event?.data?.phase;
362
+ // 仅 phase=end 是 run 级终态信号;phase=error 可能是单次模型调用
363
+ // 失败(后续还有重试/start),不能据此放行 announce。
364
+ // 真正失败的 run(无 end)由 120s 安全超时兜底 flush 缓冲文本。
365
+ if (phase !== "end")
366
+ return;
367
+ const sessionKey = event?.sessionKey ?? "";
368
+ if (sessionKey.startsWith("cron:") || sessionKey.includes(":cron:") || isCronActive()) {
369
+ logger.log(`[XY-CRON] Cron run lifecycle end, sessionKey=${sessionKey || "unknown"}`);
370
+ notifyCronAgentEnd();
371
+ }
372
+ }
373
+ catch (err) {
374
+ logger.error(`[XY-CRON] Error in cron lifecycle subscription:`, err);
375
+ }
376
+ },
377
+ });
356
378
  // SKILL RETRIEVER HOOK: before_prompt_build hook
357
379
  const pluginConfig = api.pluginConfig || {};
358
380
  const skillRetrieverConfig = normalizeToolRetrieverConfig({
package/dist/src/bot.js CHANGED
@@ -313,7 +313,7 @@ export async function handleXYMessage(params) {
313
313
  // per-session ReplyOperation lock in admitReplyTurn which would otherwise
314
314
  // block the steer message until the first message completes — making steer
315
315
  // indistinguishable from a followup.
316
- if (isUpdate && !skipReg && route.sessionKey) {
316
+ if (isUpdate && route.sessionKey) {
317
317
  const activeSessionId = resolveActiveEmbeddedRunSessionId(route.sessionKey);
318
318
  if (activeSessionId) {
319
319
  log.log(`[BOT-STEER] Direct steer attempt: activeSessionId=${activeSessionId}, textLen=${textForAgent.length}`);
@@ -486,7 +486,7 @@ export async function handleXYMessage(params) {
486
486
  // signal init complete to release the global dispatch gate
487
487
  // for the next session.
488
488
  const dispatchPromise = runWithSessionContext(sessionContext, async () => {
489
- const isSteerDispatch = isUpdate && !skipReg;
489
+ const isSteerDispatch = isUpdate;
490
490
  log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=${isSteerDispatch}`);
491
491
  log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
492
492
  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";
@@ -158,7 +158,7 @@ export const xyPlugin = {
158
158
  },
159
159
  gateway: {
160
160
  async startAccount(context) {
161
- const { monitorXYProvider } = await import("./monitor.js");
161
+ const { monitorXYProvider } = await import("./dispatch/monitor.js");
162
162
  const { createXyAcpBindingManager } = await import("./acp-session-binding.js");
163
163
  const account = resolveXYConfig(context.cfg);
164
164
  context.setStatus?.({
@@ -0,0 +1,87 @@
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
+ * 绑定 openclaw sessionKey 到首条消息的 A2A 身份。
34
+ * 仅在首条消息时调用(steer 消息不覆盖,保持 subagent 等待态跟踪稳定)。
35
+ */
36
+ export declare function bindSessionKey(sessionKey: string, sessionId: string, taskId: string, messageId: string): void;
37
+ export declare function resolveBySessionKey(sessionKey: string): {
38
+ sessionId: string;
39
+ taskId: string;
40
+ messageId: string;
41
+ } | null;
42
+ export declare function unbindSessionKey(sessionKey: string): void;
43
+ /**
44
+ * 全局缓存 resolved XY config,供 hook 上下文(无 raw ClawdbotConfig)使用。
45
+ */
46
+ export declare function cacheXYConfig(config: unknown): void;
47
+ export declare function getCachedXYConfig(): unknown;
48
+ /**
49
+ * 启动(或重启)session 的 30s 状态心跳。
50
+ * 心跳由 manager 拥有:dispatcher 销毁不影响心跳;
51
+ * tick 时读取任务链末尾的当前 taskId/messageId。
52
+ */
53
+ export declare function startStatusHeartbeat(sessionId: string, text?: string): void;
54
+ export declare function stopStatusHeartbeat(sessionId: string): void;
55
+ /**
56
+ * subagent_spawned hook 调用。递增 expected completion 计数。
57
+ * 若等待态尚不存在则创建(sessions_spawn 可能先于 sessions_yield)。
58
+ */
59
+ export declare function markSubagentSpawned(sessionKey: string): number;
60
+ /**
61
+ * subagent_ended hook 调用。标记一个 subagent run 结束并计为一次交付。
62
+ * 这是主要的完成跟踪机制(subagent 完成可能不经 xyOutbound.sendText)。
63
+ */
64
+ export declare function markSubagentEnded(sessionKey: string): SubagentWaitTransition | null;
65
+ /**
66
+ * bot.ts onSettled 调用。标记父 dispatcher 已 settle。
67
+ */
68
+ export declare function markParentSettled(sessionId: string, taskId: string): SubagentWaitTransition | null;
69
+ /**
70
+ * 存储从 xyOutbound.sendText 捕获的完成文本。不递增交付计数。
71
+ */
72
+ export declare function addCompletionText(sessionId: string, taskId: string, text: string): void;
73
+ export declare function getWaitState(sessionId: string, taskId?: string): SubagentWaitState | null;
74
+ export declare function hasWaitState(sessionId: string, taskId?: string): boolean;
75
+ export declare function clearWaitState(sessionId: string, reason: string, taskId?: string): SubagentWaitState | null;
76
+ /**
77
+ * 当所有 subagent 完成且父 turn 已 settle 时,由 manager 发送唯一的
78
+ * final 帧并真正结束当前 A2A session:
79
+ * 1. 合并 completionTexts 发送 final:true 帧
80
+ * 2. 清理等待态、停止心跳、移除任务链
81
+ * 3. 会话状态迁移到 completed
82
+ */
83
+ export declare function deliverSubagentFinalResult(params: {
84
+ state: SubagentWaitState;
85
+ reason?: string;
86
+ text?: string;
87
+ }): Promise<void>;
@@ -0,0 +1,387 @@
1
+ // 对话管理层 —— ConversationManager。
2
+ //
3
+ // 收拢 openclaw 与 xy server 之间的会话状态与生命周期管理:
4
+ // - session 注册表(globalThis 单例,跨模块去重存活)
5
+ // - task 身份链(吸收自原 task-manager.ts)
6
+ // - sessionKey ↔ sessionId 索引(吸收自原 subagent-wait-state.ts 的 sessionKeyMap)
7
+ // - subagent 等待态(吸收自原 subagent-wait-state.ts)
8
+ // - XY config 缓存(吸收自原 __xyCachedXYConfig)
9
+ //
10
+ // 依赖方向约束:本模块只允许依赖 conversation-session / utils / types,
11
+ // 禁止 import dispatch 层(bot/reply-dispatcher/monitor/outbound),避免循环。
12
+ import { randomUUID } from "crypto";
13
+ import { logger } from "../utils/logger.js";
14
+ import { sendStatusUpdate, sendA2AResponse } from "../formatter.js";
15
+ import { createConversationSession, } from "./conversation-session.js";
16
+ // ─── Global registry(globalThis 单例) ───────────────────────
17
+ const _g = globalThis;
18
+ if (!_g.__xyConversationSessions) {
19
+ _g.__xyConversationSessions = new Map();
20
+ }
21
+ if (!_g.__xySessionKeyIndex) {
22
+ // sessionKey → 首条消息的 { sessionId, taskId, messageId }。
23
+ // 注意:taskId/messageId 刻意保留首条消息的值(steer 消息不覆盖),
24
+ // 与原 registerSessionKeyMapping 语义一致。
25
+ _g.__xySessionKeyIndex = new Map();
26
+ }
27
+ if (!_g.__xyConversationCachedConfig) {
28
+ _g.__xyConversationCachedConfig = null;
29
+ }
30
+ const sessions = _g.__xyConversationSessions;
31
+ const sessionKeyIndex = _g.__xySessionKeyIndex;
32
+ // ─── Session registry ─────────────────────────────────────────
33
+ export function getOrCreateSession(sessionId) {
34
+ let session = sessions.get(sessionId);
35
+ if (!session) {
36
+ session = createConversationSession(sessionId);
37
+ sessions.set(sessionId, session);
38
+ }
39
+ session.lastActivityAt = Date.now();
40
+ return session;
41
+ }
42
+ export function getSession(sessionId) {
43
+ return sessions.get(sessionId) ?? null;
44
+ }
45
+ export function deleteSession(sessionId) {
46
+ sessions.delete(sessionId);
47
+ }
48
+ export function listSessions() {
49
+ return Array.from(sessions.values());
50
+ }
51
+ /** 会话状态迁移(working / waiting-subagent / completing / 终态)。 */
52
+ export function setSessionState(sessionId, state) {
53
+ const session = sessions.get(sessionId);
54
+ if (session) {
55
+ session.state = state;
56
+ session.lastActivityAt = Date.now();
57
+ }
58
+ }
59
+ // ─── Task 身份链(吸收自原 task-manager.ts) ──────────────────
60
+ /**
61
+ * 注册或更新 session 的活跃 taskId。
62
+ * Returns true if this was an update (session already had an active task).
63
+ */
64
+ export function registerTask(sessionId, taskId, messageId) {
65
+ const session = getOrCreateSession(sessionId);
66
+ const isUpdate = session.tasks.length > 0;
67
+ if (isUpdate) {
68
+ logger.log(`[TASK_MANAGER] Updating taskId: ${session.tasks[session.tasks.length - 1].taskId} → ${taskId}`);
69
+ }
70
+ else {
71
+ logger.log(`[TASK_MANAGER] Registered new taskId: ${taskId}`);
72
+ }
73
+ session.tasks = session.tasks.filter((entry) => entry.taskId !== taskId);
74
+ session.tasks.push({ taskId, messageId, updatedAt: Date.now() });
75
+ return isUpdate;
76
+ }
77
+ /**
78
+ * 移除 session 的活跃 taskId(消息处理完成时调用)。
79
+ * @param expectedTaskId 可选:精确移除指定的 taskId,而非清空整个任务链。
80
+ */
81
+ export function completeTask(sessionId, expectedTaskId) {
82
+ const session = sessions.get(sessionId);
83
+ if (!session)
84
+ return;
85
+ if (!expectedTaskId) {
86
+ logger.log(`[TASK_MANAGER] Removing taskId`);
87
+ session.tasks = [];
88
+ return;
89
+ }
90
+ const nextTasks = session.tasks.filter((entry) => entry.taskId !== expectedTaskId);
91
+ if (nextTasks.length === session.tasks.length) {
92
+ const current = session.tasks[session.tasks.length - 1];
93
+ logger.log(`[TASK_MANAGER] Preserving taskId ${current?.taskId}; completed task ${expectedTaskId} is not active`);
94
+ return;
95
+ }
96
+ session.tasks = nextTasks;
97
+ if (nextTasks.length > 0) {
98
+ logger.log(`[TASK_MANAGER] Removed taskId ${expectedTaskId}, restored current taskId ${nextTasks[nextTasks.length - 1].taskId}`);
99
+ }
100
+ else {
101
+ logger.log(`[TASK_MANAGER] Removing taskId (last task ${expectedTaskId})`);
102
+ }
103
+ }
104
+ /** 获取 session 当前活跃 task(任务链末尾)。 */
105
+ export function getCurrentTask(sessionId) {
106
+ const session = sessions.get(sessionId);
107
+ if (!session || session.tasks.length === 0)
108
+ return null;
109
+ return session.tasks[session.tasks.length - 1];
110
+ }
111
+ /** 获取 session 当前活跃 taskId(reply-dispatcher 跨链读取 / 工具富化用)。 */
112
+ export function getCurrentTaskId(sessionId) {
113
+ return getCurrentTask(sessionId)?.taskId ?? null;
114
+ }
115
+ /** 获取 session 当前活跃 messageId。 */
116
+ export function getCurrentMessageId(sessionId) {
117
+ return getCurrentTask(sessionId)?.messageId ?? null;
118
+ }
119
+ export function hasActiveTask(sessionId) {
120
+ const session = sessions.get(sessionId);
121
+ return !!session && session.tasks.length > 0;
122
+ }
123
+ /** 所有活跃 task 绑定(gateway_stop 通知等场景)。 */
124
+ export function listActiveTaskBindings() {
125
+ const result = [];
126
+ for (const session of sessions.values()) {
127
+ const current = session.tasks[session.tasks.length - 1];
128
+ if (current) {
129
+ result.push({
130
+ sessionId: session.sessionId,
131
+ currentTaskId: current.taskId,
132
+ currentMessageId: current.messageId,
133
+ updatedAt: current.updatedAt,
134
+ });
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+ // ─── SessionKey ↔ SessionId 索引 ──────────────────────────────
140
+ /**
141
+ * 绑定 openclaw sessionKey 到首条消息的 A2A 身份。
142
+ * 仅在首条消息时调用(steer 消息不覆盖,保持 subagent 等待态跟踪稳定)。
143
+ */
144
+ export function bindSessionKey(sessionKey, sessionId, taskId, messageId) {
145
+ if (!sessionKey || !sessionId)
146
+ return;
147
+ sessionKeyIndex.set(sessionKey, { sessionId, taskId, messageId });
148
+ const session = getOrCreateSession(sessionId);
149
+ session.sessionKey = sessionKey;
150
+ }
151
+ export function resolveBySessionKey(sessionKey) {
152
+ return sessionKeyIndex.get(sessionKey) ?? null;
153
+ }
154
+ export function unbindSessionKey(sessionKey) {
155
+ sessionKeyIndex.delete(sessionKey);
156
+ }
157
+ // ─── XY config 缓存 ───────────────────────────────────────────
158
+ /**
159
+ * 全局缓存 resolved XY config,供 hook 上下文(无 raw ClawdbotConfig)使用。
160
+ */
161
+ export function cacheXYConfig(config) {
162
+ _g.__xyConversationCachedConfig = config;
163
+ }
164
+ export function getCachedXYConfig() {
165
+ return _g.__xyConversationCachedConfig;
166
+ }
167
+ // ─── 30s 状态心跳(manager 拥有,独立于 dispatcher 生命周期) ──
168
+ const STATUS_HEARTBEAT_INTERVAL_MS = 30 * 1000;
169
+ const DEFAULT_STATUS_TEXT = "任务正在处理中,请稍候~";
170
+ /**
171
+ * 启动(或重启)session 的 30s 状态心跳。
172
+ * 心跳由 manager 拥有:dispatcher 销毁不影响心跳;
173
+ * tick 时读取任务链末尾的当前 taskId/messageId。
174
+ */
175
+ export function startStatusHeartbeat(sessionId, text = DEFAULT_STATUS_TEXT) {
176
+ const session = getOrCreateSession(sessionId);
177
+ if (session.statusTimer) {
178
+ clearInterval(session.statusTimer);
179
+ }
180
+ logger.withContext(sessionId, "").log(`[STATUS-HEARTBEAT] Starting manager-owned heartbeat, text="${text}"`);
181
+ session.statusTimer = setInterval(() => {
182
+ const current = getCurrentTask(sessionId);
183
+ const config = getCachedXYConfig();
184
+ if (!current || !config) {
185
+ return;
186
+ }
187
+ void sendStatusUpdate({
188
+ config,
189
+ sessionId,
190
+ taskId: current.taskId,
191
+ messageId: current.messageId,
192
+ text,
193
+ state: "working",
194
+ }).catch((err) => {
195
+ logger.withContext(sessionId, current.taskId).error(`Failed to send status update:`, err);
196
+ });
197
+ }, STATUS_HEARTBEAT_INTERVAL_MS);
198
+ }
199
+ export function stopStatusHeartbeat(sessionId) {
200
+ const session = sessions.get(sessionId);
201
+ if (session?.statusTimer) {
202
+ logger.withContext(sessionId, "").log(`[STATUS-HEARTBEAT] Stopping manager-owned heartbeat`);
203
+ clearInterval(session.statusTimer);
204
+ session.statusTimer = undefined;
205
+ }
206
+ }
207
+ // ─── Subagent 等待态(吸收自原 subagent-wait-state.ts) ───────
208
+ const WAIT_TTL_MS = 30 * 60 * 1000; // 30 minutes
209
+ function isExpired(state) {
210
+ return Date.now() - state.startedAt > WAIT_TTL_MS;
211
+ }
212
+ function isComplete(state) {
213
+ return state.deliveredCompletions >= Math.max(1, state.expectedCompletions);
214
+ }
215
+ function claimFinalizationIfReady(state) {
216
+ if (state.finalizationClaimed)
217
+ return false;
218
+ if (!state.parentSettled || !isComplete(state))
219
+ return false;
220
+ state.finalizationClaimed = true;
221
+ return true;
222
+ }
223
+ function getStatesArray(sessionId) {
224
+ const session = sessions.get(sessionId);
225
+ if (!session)
226
+ return [];
227
+ const active = session.subagentWaits.filter((s) => {
228
+ if (!isExpired(s))
229
+ return true;
230
+ stopStatusHeartbeat(sessionId);
231
+ logger.withContext(sessionId, s.taskId).log(`[SUBAGENT-WAIT] Expired wait state cleared, expected=${s.expectedCompletions}, delivered=${s.deliveredCompletions}`);
232
+ return false;
233
+ });
234
+ session.subagentWaits = active;
235
+ return active;
236
+ }
237
+ /**
238
+ * subagent_spawned hook 调用。递增 expected completion 计数。
239
+ * 若等待态尚不存在则创建(sessions_spawn 可能先于 sessions_yield)。
240
+ */
241
+ export function markSubagentSpawned(sessionKey) {
242
+ const mapped = resolveBySessionKey(sessionKey);
243
+ if (!mapped) {
244
+ logger.log(`[SUBAGENT-WAIT] No session mapping for sessionKey=${sessionKey.slice(0, 30)}`);
245
+ return 0;
246
+ }
247
+ const { sessionId, taskId, messageId } = mapped;
248
+ const session = getOrCreateSession(sessionId);
249
+ const states = getStatesArray(sessionId);
250
+ let state = states.find((s) => s.taskId === taskId);
251
+ if (!state) {
252
+ state = {
253
+ sessionId,
254
+ sessionKey,
255
+ taskId,
256
+ messageId,
257
+ artifactId: randomUUID(),
258
+ startedAt: Date.now(),
259
+ expectedCompletions: 0,
260
+ deliveredCompletions: 0,
261
+ completionTexts: [],
262
+ parentSettled: false,
263
+ finalizationClaimed: false,
264
+ };
265
+ states.push(state);
266
+ }
267
+ state.expectedCompletions = Math.max(1, state.expectedCompletions + 1);
268
+ session.subagentWaits = states;
269
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent spawned, expected=${state.expectedCompletions}`);
270
+ return state.expectedCompletions;
271
+ }
272
+ /**
273
+ * subagent_ended hook 调用。标记一个 subagent run 结束并计为一次交付。
274
+ * 这是主要的完成跟踪机制(subagent 完成可能不经 xyOutbound.sendText)。
275
+ */
276
+ export function markSubagentEnded(sessionKey) {
277
+ const mapped = resolveBySessionKey(sessionKey);
278
+ if (!mapped)
279
+ return null;
280
+ const { sessionId, taskId } = mapped;
281
+ const session = getOrCreateSession(sessionId);
282
+ const states = getStatesArray(sessionId);
283
+ const state = states.find((s) => s.taskId === taskId);
284
+ if (!state) {
285
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended but no wait state found`);
286
+ return null;
287
+ }
288
+ state.deliveredCompletions += 1;
289
+ const complete = isComplete(state);
290
+ const shouldFinalize = claimFinalizationIfReady(state);
291
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended, delivered=${state.deliveredCompletions}/${state.expectedCompletions}, complete=${complete}, shouldFinalize=${shouldFinalize}`);
292
+ return { state, isComplete: complete, shouldFinalize };
293
+ }
294
+ /**
295
+ * bot.ts onSettled 调用。标记父 dispatcher 已 settle。
296
+ */
297
+ export function markParentSettled(sessionId, taskId) {
298
+ const session = getOrCreateSession(sessionId);
299
+ const states = getStatesArray(sessionId);
300
+ const state = states.find((s) => s.taskId === taskId);
301
+ if (!state) {
302
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] No wait state matched for parent settled`);
303
+ return null;
304
+ }
305
+ state.parentSettled = true;
306
+ const complete = isComplete(state);
307
+ const shouldFinalize = claimFinalizationIfReady(state);
308
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Parent settled, completions=${state.deliveredCompletions}/${state.expectedCompletions}, shouldFinalize=${shouldFinalize}`);
309
+ return { state, isComplete: complete, shouldFinalize };
310
+ }
311
+ /**
312
+ * 存储从 xyOutbound.sendText 捕获的完成文本。不递增交付计数。
313
+ */
314
+ export function addCompletionText(sessionId, taskId, text) {
315
+ const states = getStatesArray(sessionId);
316
+ const state = states.find((s) => s.taskId === taskId);
317
+ if (!state || !text.trim())
318
+ return;
319
+ state.completionTexts.push(text.trim());
320
+ }
321
+ export function getWaitState(sessionId, taskId) {
322
+ const states = getStatesArray(sessionId);
323
+ if (states.length === 0)
324
+ return null;
325
+ if (taskId) {
326
+ return states.find((s) => s.taskId === taskId) ?? null;
327
+ }
328
+ // FIFO: 未指定 taskId 时返回最早的等待态
329
+ return states[0] ?? null;
330
+ }
331
+ export function hasWaitState(sessionId, taskId) {
332
+ return getWaitState(sessionId, taskId) !== null;
333
+ }
334
+ export function clearWaitState(sessionId, reason, taskId) {
335
+ const session = sessions.get(sessionId);
336
+ if (!session)
337
+ return null;
338
+ const states = getStatesArray(sessionId);
339
+ if (states.length === 0)
340
+ return null;
341
+ const index = taskId
342
+ ? states.findIndex((s) => s.taskId === taskId)
343
+ : 0;
344
+ if (index < 0) {
345
+ logger.withContext(sessionId, taskId ?? "").log(`[SUBAGENT-WAIT] No wait state matched for clear, reason=${reason}`);
346
+ return null;
347
+ }
348
+ const [state] = states.splice(index, 1);
349
+ stopStatusHeartbeat(sessionId);
350
+ session.subagentWaits = states;
351
+ logger.withContext(sessionId, state.taskId).log(`[SUBAGENT-WAIT] Cleared wait state, reason=${reason}, remaining=${states.length}`);
352
+ return state;
353
+ }
354
+ // ─── Subagent 最终交付(收拢自原 outbound.deliverSubagentFinalResult) ──
355
+ /**
356
+ * 当所有 subagent 完成且父 turn 已 settle 时,由 manager 发送唯一的
357
+ * final 帧并真正结束当前 A2A session:
358
+ * 1. 合并 completionTexts 发送 final:true 帧
359
+ * 2. 清理等待态、停止心跳、移除任务链
360
+ * 3. 会话状态迁移到 completed
361
+ */
362
+ export async function deliverSubagentFinalResult(params) {
363
+ const { state, reason } = params;
364
+ const log = logger.withContext(state.sessionId, state.taskId);
365
+ const config = getCachedXYConfig();
366
+ if (!config) {
367
+ log.error(`[SUBAGENT-FINAL] No cached XY config, cannot deliver final result`);
368
+ return;
369
+ }
370
+ const finalText = state.completionTexts.length > 0
371
+ ? state.completionTexts.join("\n\n")
372
+ : (params.text || undefined) ?? "子任务已完成";
373
+ setSessionState(state.sessionId, "completing");
374
+ await sendA2AResponse({
375
+ config,
376
+ sessionId: state.sessionId,
377
+ taskId: state.taskId,
378
+ messageId: state.messageId,
379
+ text: finalText,
380
+ append: false,
381
+ final: true,
382
+ });
383
+ clearWaitState(state.sessionId, reason ?? "all-subagent-results-delivered", state.taskId);
384
+ completeTask(state.sessionId, state.taskId);
385
+ setSessionState(state.sessionId, "completed");
386
+ log.log(`[SUBAGENT-FINAL] Subagent final delivered to original A2A task, reason=${reason}`);
387
+ }
@@ -0,0 +1,38 @@
1
+ export interface TaskEntry {
2
+ taskId: string;
3
+ messageId: string;
4
+ updatedAt: number;
5
+ }
6
+ export type SessionState = "working" | "waiting-subagent" | "completing" | "completed" | "failed" | "canceled";
7
+ export interface SubagentWaitState {
8
+ sessionId: string;
9
+ sessionKey: string;
10
+ taskId: string;
11
+ messageId: string;
12
+ artifactId: string;
13
+ startedAt: number;
14
+ expectedCompletions: number;
15
+ deliveredCompletions: number;
16
+ completionTexts: string[];
17
+ parentSettled: boolean;
18
+ finalizationClaimed: boolean;
19
+ }
20
+ export interface SubagentWaitTransition {
21
+ state: SubagentWaitState;
22
+ isComplete: boolean;
23
+ shouldFinalize: boolean;
24
+ }
25
+ export interface ConversationSession {
26
+ /** A2A sessionId —— 注册表 key。 */
27
+ sessionId: string;
28
+ state: SessionState;
29
+ /** taskId/messageId 链,最后一个为当前任务。 */
30
+ tasks: TaskEntry[];
31
+ /** openclaw 路由 sessionKey(首条消息时绑定)。 */
32
+ sessionKey?: string;
33
+ subagentWaits: SubagentWaitState[];
34
+ /** manager 拥有的 30s 状态心跳定时器。 */
35
+ statusTimer?: NodeJS.Timeout;
36
+ lastActivityAt: number;
37
+ }
38
+ export declare function createConversationSession(sessionId: string): ConversationSession;
@@ -0,0 +1,17 @@
1
+ // 对话管理层 —— 会话状态对象定义。
2
+ //
3
+ // ConversationSession 是对话管理层的核心状态载体,收拢了重构前散落在
4
+ // task-manager / subagent-wait-state / reply-dispatcher 闭包中的会话状态:
5
+ // - tasks: session 的 taskId/messageId 链(吸收自 task-manager)
6
+ // - sessionKey: openclaw 路由 sessionKey(吸收自 sessionKeyMap)
7
+ // - subagentWaits: subagent 等待态(吸收自 subagent-wait-state)
8
+ // - statusTimer: 30s 状态心跳定时器(由 manager 拥有)
9
+ export function createConversationSession(sessionId) {
10
+ return {
11
+ sessionId,
12
+ state: "working",
13
+ tasks: [],
14
+ subagentWaits: [],
15
+ lastActivityAt: Date.now(),
16
+ };
17
+ }
@@ -0,0 +1,25 @@
1
+ import type { XYChannelConfig } from "../types.js";
2
+ /** 关闭当前 cron turn(清理全部定时器)。 */
3
+ export declare function closeCronTurn(reason: string): void;
4
+ /**
5
+ * cron turn 开始检测。由 provider.ts 在识别到 [cron:...] 首条消息时调用。
6
+ * 同一 cron run 的每次模型调用都会重复触发检测 —— 此时仅刷新安全定时器,
7
+ * 不重开 turn(避免 churn,也避免重置 agentEnded/缓冲状态)。
8
+ */
9
+ export declare function notifyCronDetected(jobId?: string): void;
10
+ /**
11
+ * cron run 结束信号(lifecycle phase=end,由 index.ts 的 agent event
12
+ * subscription 调用)。进入 announce 宽限期:
13
+ * - 宽限期内 announce 到达 → 放行(见 gateCronSendText);
14
+ * - 宽限期结束无 announce → 合并缓冲文本发一条(路径 B)。
15
+ */
16
+ export declare function notifyCronAgentEnd(): void;
17
+ export type CronSendTextGate = "swallow" | "announce" | "none";
18
+ /**
19
+ * sendText 闸门:
20
+ * - "swallow":cron turn 进行中(run 未结束),吞掉中间 sendText,文本入缓冲;
21
+ * - "announce":run 已结束(宽限期内),当前 sendText 是 announce 最终
22
+ * 结果,放行(唯一一条,缓冲丢弃);
23
+ * - "none":无 cron turn,正常发送。
24
+ */
25
+ export declare function gateCronSendText(swallowedText?: string, config?: XYChannelConfig): CronSendTextGate;