@ynhcj/xiaoyi-channel 0.0.188-next → 0.0.189-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.
package/dist/src/bot.js CHANGED
@@ -16,7 +16,7 @@ import { saveRuntimeInfo } from "./utils/runtime-manager.js";
16
16
  import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
17
17
  import { setCsplSteerContext } from "./cspl/steer-context.js";
18
18
  import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
19
- import { registerSessionKeyMapping, getWaitState, markParentSettled, } from "./subagent-wait-state.js";
19
+ import { registerSessionKeyMapping, getWaitState, hasWaitState, markParentSettled, } from "./subagent-wait-state.js";
20
20
  import { logger } from "./utils/logger.js";
21
21
  /**
22
22
  * Handle an incoming A2A message.
@@ -375,10 +375,14 @@ export async function handleXYMessage(params) {
375
375
  const cleanup = () => {
376
376
  if (cleaned)
377
377
  return;
378
- const pendingWait = getWaitState(parsed.sessionId, parsed.taskId);
378
+ // Check for pending subagent wait on this session (any taskId).
379
+ // Steer dispatches have a different taskId, so we check both
380
+ // exact match and session-wide presence.
381
+ const pendingWait = getWaitState(parsed.sessionId, parsed.taskId) ??
382
+ (hasWaitState(parsed.sessionId) ? { deliveredCompletions: 0, expectedCompletions: 1 } : null);
379
383
  if (pendingWait && pendingWait.deliveredCompletions < pendingWait.expectedCompletions) {
380
384
  // Subagent wait active — skip cleanup, session stays alive
381
- log.log(`[BOT] Cleanup suppressed — subagent wait active ${pendingWait.deliveredCompletions}/${pendingWait.expectedCompletions}`);
385
+ log.log(`[BOT] Cleanup suppressed — subagent wait active on session, taskId=${parsed.taskId}`);
382
386
  return;
383
387
  }
384
388
  cleaned = true;
@@ -140,31 +140,49 @@ export const xyOutbound = {
140
140
  // Resolve configuration
141
141
  const config = resolveXYConfig(cfg);
142
142
  // ── Subagent completion interception ─────────────────────────
143
- // Isolation guards:
144
- // 1. ALS context is null (subagent completions arrive outside any agent turn)
145
- // 2. A wait state exists for the target sessionId
146
- // 3. The taskId matches (enhanced by resolveTarget from wait state)
147
- const alsCtx = getCurrentSessionContext();
148
- if (!alsCtx && to && to !== DEFAULT_PUSH_MARKER) {
143
+ // Detect subagent completions by checking if the resolved target
144
+ // matches a wait state's sessionId + taskId. This works regardless
145
+ // of ALS context because:
146
+ // - Normal agent turns: ALS enhances target with CURRENT taskId,
147
+ // which differs from wait state's taskId (no match → skip)
148
+ // - Subagent completions: ALS is null, resolveTarget enhanced
149
+ // from wait state's taskId (match → intercept)
150
+ // - Cron: to=DEFAULT_PUSH_MARKER (no match)
151
+ // - Message tool: ALS enhances with current taskId (no match
152
+ // unless the agent happens to be on the same yielded task)
153
+ if (to && to !== DEFAULT_PUSH_MARKER) {
149
154
  const [targetSessionId, targetTaskId] = String(to).split("::");
150
155
  const waitState = getWaitState(targetSessionId, targetTaskId);
151
- if (waitState && !waitState.finalizationClaimed) {
156
+ const alsCtx = getCurrentSessionContext();
157
+ // Only intercept when ALS is null OR the target taskId matches
158
+ // the wait state (not the current ALS taskId). This ensures
159
+ // we don't intercept normal agent turn deliveries.
160
+ const isFromWaitState = waitState &&
161
+ (!alsCtx || alsCtx.taskId !== targetTaskId);
162
+ if (isFromWaitState && !waitState.finalizationClaimed) {
163
+ const log = logger.withContext(waitState.sessionId, waitState.taskId);
164
+ log.log(`[xyOutbound.sendText] Subagent completion detected, als=${!!alsCtx}, delivered=${waitState.deliveredCompletions}/${waitState.expectedCompletions}`);
152
165
  const delivered = markCompletionDelivered(targetSessionId, targetTaskId, text);
153
166
  if (delivered) {
154
167
  const { state, isComplete, shouldFinalize } = delivered;
155
- const log = logger.withContext(state.sessionId, state.taskId);
156
168
  if (shouldFinalize) {
157
- // All completions arrived and parent already settled → send final
158
- log.log(`[xyOutbound.sendText] All subagent results arrived after parent settled, finalizing`);
169
+ // All completions arrived and parent already settled → finalize
170
+ log.log(`[xyOutbound.sendText] Finalizing subagent results`);
159
171
  await deliverSubagentFinalResult({ config, state, reason: "all-subagent-results-delivered-after-parent-settled", text: text });
172
+ // Subagent completion delivered via A2A, skip push
173
+ return {
174
+ channel: "xiaoyi-channel",
175
+ messageId: state.artifactId,
176
+ chatId: to,
177
+ };
160
178
  }
161
- else if (isComplete) {
162
- // All completions arrived but parent hasn't settled yet → defer
179
+ if (isComplete) {
180
+ // All completions arrived but parent hasn't settled yet → defer to onSettled
163
181
  log.log(`[xyOutbound.sendText] All subagent results arrived before parent settled, deferring finalization`);
164
182
  }
165
183
  else {
166
- // Still waiting for more completions → send progress update
167
- log.log(`[xyOutbound.sendText] Subagent completion ${state.deliveredCompletions}/${state.expectedCompletions}`);
184
+ // Still waiting for more completions → send progress via A2A, skip push
185
+ log.log(`[xyOutbound.sendText] Subagent progress ${state.deliveredCompletions}/${state.expectedCompletions}`);
168
186
  try {
169
187
  await sendStatusUpdate({
170
188
  config,
@@ -181,7 +199,12 @@ export const xyOutbound = {
181
199
  }
182
200
  }
183
201
  }
184
- // Continue with push delivery (subagent result also delivered as push)
202
+ // Skip push delivery for subagent completions only send via A2A
203
+ return {
204
+ channel: "xiaoyi-channel",
205
+ messageId: waitState.artifactId,
206
+ chatId: to,
207
+ };
185
208
  }
186
209
  }
187
210
  // ── End subagent interception ───────────────────────────────
@@ -463,7 +463,12 @@ export function createXYReplyDispatcher(params) {
463
463
  else {
464
464
  // 新文本不以已累积内容为前缀(如工具调用后模型重新开始生成),
465
465
  // 更新 accumulatedText 为当前文本,后续基于此新前缀做去重
466
+ const wasFirstRound = accumulatedText.length === 0;
466
467
  accumulatedText = "";
468
+ // 新一轮输出前加换行分隔(第一轮除外)
469
+ if (sendText.length > 0 && !wasFirstRound) {
470
+ sendText = "\n" + sendText;
471
+ }
467
472
  }
468
473
  accumulatedText += sendText;
469
474
  hasSentResponse = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.188-next",
3
+ "version": "0.0.189-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",