@ynhcj/xiaoyi-channel 0.0.189-next → 0.0.190-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/index.js CHANGED
@@ -181,14 +181,27 @@ function registerSubagentHooks(api) {
181
181
  }
182
182
  });
183
183
  // subagent_ended: fires when a subagent run terminates (complete/error/killed).
184
- // We log the event for tracking; the actual completion count is decremented
185
- // when the subagent result is delivered via xyOutbound.sendText.
184
+ // This is the PRIMARY delivery tracking mechanism. When all expected
185
+ // subagents have ended and parent has settled, we finalize the A2A session.
186
186
  api.on("subagent_ended", async (event, ctx) => {
187
187
  const requesterSessionKey = ctx?.requesterSessionKey;
188
188
  if (!requesterSessionKey)
189
189
  return;
190
- markSubagentEnded(requesterSessionKey);
191
- console.log(`[XY-SUBAGENT] ended, sessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome}, requester=${requesterSessionKey.slice(0, 30)}`);
190
+ const transition = markSubagentEnded(requesterSessionKey);
191
+ console.log(`[XY-SUBAGENT] ended, sessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome}, complete=${transition?.isComplete ?? false}, shouldFinalize=${transition?.shouldFinalize ?? false}`);
192
+ if (transition?.shouldFinalize) {
193
+ // All subagents ended and parent already settled — finalize A2A session
194
+ const { resolveXYConfig } = await import("./src/config.js");
195
+ const { deliverSubagentFinalResult } = await import("./src/outbound.js");
196
+ const { getXYRuntime } = await import("./src/runtime.js");
197
+ const config = resolveXYConfig(getXYRuntime().config);
198
+ await deliverSubagentFinalResult({
199
+ config,
200
+ state: transition.state,
201
+ reason: "all-subagents-ended-after-parent-settled",
202
+ });
203
+ console.log(`[XY-SUBAGENT] Finalized A2A session after all subagents ended`);
204
+ }
192
205
  });
193
206
  }
194
207
  function registerFullHooks(api) {
@@ -6,7 +6,7 @@ import { resolveXYConfig } from "./config.js";
6
6
  import { XYFileUploadService } from "./file-upload.js";
7
7
  import { XYPushService } from "./push.js";
8
8
  import { getCurrentSessionContext } from "./tools/session-manager.js";
9
- import { getWaitState, markCompletionDelivered, clearWaitState, } from "./subagent-wait-state.js";
9
+ import { getWaitState, addCompletionText, clearWaitState, } from "./subagent-wait-state.js";
10
10
  import { decrementTaskIdRef } from "./task-manager.js";
11
11
  import { sendA2AResponse, sendStatusUpdate } from "./formatter.js";
12
12
  import { savePushData } from "./utils/pushdata-manager.js";
@@ -139,67 +139,23 @@ export const xyOutbound = {
139
139
  sendText: async ({ cfg, to, text, accountId }) => {
140
140
  // Resolve configuration
141
141
  const config = resolveXYConfig(cfg);
142
- // ── Subagent completion interception ─────────────────────────
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)
142
+ // ── Subagent completion text capture ─────────────────────────
143
+ // Subagent completions may arrive here when openclaw's announce
144
+ // flow delivers through the channel outbound. We capture the text
145
+ // for the final A2A response but do NOT track delivery count here.
146
+ // Delivery tracking is done by the subagent_ended hook (index.ts).
153
147
  if (to && to !== DEFAULT_PUSH_MARKER) {
154
148
  const [targetSessionId, targetTaskId] = String(to).split("::");
155
149
  const waitState = getWaitState(targetSessionId, targetTaskId);
156
150
  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
151
  const isFromWaitState = waitState &&
161
152
  (!alsCtx || alsCtx.taskId !== targetTaskId);
162
153
  if (isFromWaitState && !waitState.finalizationClaimed) {
163
154
  const log = logger.withContext(waitState.sessionId, waitState.taskId);
164
- log.log(`[xyOutbound.sendText] Subagent completion detected, als=${!!alsCtx}, delivered=${waitState.deliveredCompletions}/${waitState.expectedCompletions}`);
165
- const delivered = markCompletionDelivered(targetSessionId, targetTaskId, text);
166
- if (delivered) {
167
- const { state, isComplete, shouldFinalize } = delivered;
168
- if (shouldFinalize) {
169
- // All completions arrived and parent already settled → finalize
170
- log.log(`[xyOutbound.sendText] Finalizing subagent results`);
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
- };
178
- }
179
- if (isComplete) {
180
- // All completions arrived but parent hasn't settled yet → defer to onSettled
181
- log.log(`[xyOutbound.sendText] All subagent results arrived before parent settled, deferring finalization`);
182
- }
183
- else {
184
- // Still waiting for more completions → send progress via A2A, skip push
185
- log.log(`[xyOutbound.sendText] Subagent progress ${state.deliveredCompletions}/${state.expectedCompletions}`);
186
- try {
187
- await sendStatusUpdate({
188
- config,
189
- sessionId: state.sessionId,
190
- taskId: state.taskId,
191
- messageId: state.messageId,
192
- text: `已收到子任务结果 ${state.deliveredCompletions}/${state.expectedCompletions},继续等待其他子任务~`,
193
- state: "working",
194
- useLatestTask: false,
195
- });
196
- }
197
- catch (err) {
198
- log.error(`[xyOutbound.sendText] Failed to send subagent progress status:`, err);
199
- }
200
- }
201
- }
202
- // Skip push delivery for subagent completions — only send via A2A
155
+ log.log(`[xyOutbound.sendText] Subagent completion text captured, len=${text?.length ?? 0}`);
156
+ // Store the text for later use in final response
157
+ addCompletionText(targetSessionId, targetTaskId, text);
158
+ // Skip push delivery subagent completions go through A2A
203
159
  return {
204
160
  channel: "xiaoyi-channel",
205
161
  messageId: waitState.artifactId,
@@ -207,7 +163,7 @@ export const xyOutbound = {
207
163
  };
208
164
  }
209
165
  }
210
- // ── End subagent interception ───────────────────────────────
166
+ // ── End subagent text capture ───────────────────────────────
211
167
  // Handle default push marker (for cron jobs without explicit target)
212
168
  let actualTo = to;
213
169
  if (to === DEFAULT_PUSH_MARKER) {
@@ -41,20 +41,25 @@ export declare function unregisterSessionKeyMapping(sessionKey: string): void;
41
41
  */
42
42
  export declare function markSubagentSpawned(sessionKey: string): number;
43
43
  /**
44
- * Called from subagent_ended hook. Marks a subagent run as ended.
45
- * Does NOT decrement expected count that's done when completion is delivered via outbound.
44
+ * Called from subagent_ended hook. Marks a subagent run as ended AND
45
+ * counts it as a delivery. This is the primary completion tracking
46
+ * mechanism because subagent completions may not route through
47
+ * xyOutbound.sendText (they go through openclaw's internal gateway
48
+ * agent path with potentially deliver=false).
49
+ *
50
+ * Returns transition with shouldFinalize if all completions have ended.
46
51
  */
47
- export declare function markSubagentEnded(sessionKey: string): void;
48
- /**
49
- * Called from xyOutbound.sendText when a subagent completion is delivered.
50
- * Increments delivered count and returns transition state.
51
- */
52
- export declare function markCompletionDelivered(sessionId: string, taskId: string, text?: string): SubagentWaitTransition | null;
52
+ export declare function markSubagentEnded(sessionKey: string): SubagentWaitTransition | null;
53
53
  /**
54
54
  * Called from bot.ts onSettled. Marks parent dispatcher as settled.
55
55
  * Returns transition with shouldFinalize if all completions already arrived.
56
56
  */
57
57
  export declare function markParentSettled(sessionId: string, taskId: string): SubagentWaitTransition | null;
58
+ /**
59
+ * Store a completion text snippet captured from xyOutbound.sendText.
60
+ * Does NOT increment delivery count (that's done by markSubagentEnded).
61
+ */
62
+ export declare function addCompletionText(sessionId: string, taskId: string, text: string): void;
58
63
  export declare function attachHeartbeat(sessionId: string, taskId: string, stopHeartbeat: () => void): void;
59
64
  export declare function getWaitState(sessionId: string, taskId?: string): SubagentWaitState | null;
60
65
  export declare function hasWaitState(sessionId: string, taskId?: string): boolean;
@@ -121,39 +121,30 @@ export function markSubagentSpawned(sessionKey) {
121
121
  return state.expectedCompletions;
122
122
  }
123
123
  /**
124
- * Called from subagent_ended hook. Marks a subagent run as ended.
125
- * Does NOT decrement expected count that's done when completion is delivered via outbound.
124
+ * Called from subagent_ended hook. Marks a subagent run as ended AND
125
+ * counts it as a delivery. This is the primary completion tracking
126
+ * mechanism because subagent completions may not route through
127
+ * xyOutbound.sendText (they go through openclaw's internal gateway
128
+ * agent path with potentially deliver=false).
129
+ *
130
+ * Returns transition with shouldFinalize if all completions have ended.
126
131
  */
127
132
  export function markSubagentEnded(sessionKey) {
128
133
  const mapped = resolveSessionIdFromSessionKey(sessionKey);
129
134
  if (!mapped)
130
- return;
135
+ return null;
131
136
  const { sessionId, taskId } = mapped;
132
- const states = getStatesArray(sessionId);
133
- const state = states.find((s) => s.taskId === taskId);
134
- if (!state)
135
- return;
136
- logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended, delivered=${state.deliveredCompletions}/${state.expectedCompletions}`);
137
- }
138
- /**
139
- * Called from xyOutbound.sendText when a subagent completion is delivered.
140
- * Increments delivered count and returns transition state.
141
- */
142
- export function markCompletionDelivered(sessionId, taskId, text) {
143
137
  const states = getStatesArray(sessionId);
144
138
  const state = states.find((s) => s.taskId === taskId);
145
139
  if (!state) {
146
- logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] No wait state matched for completion delivery`);
140
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended but no wait state found`);
147
141
  return null;
148
142
  }
149
143
  state.deliveredCompletions += 1;
150
- if (text?.trim()) {
151
- state.completionTexts.push(text.trim());
152
- }
153
144
  const complete = isComplete(state);
154
145
  const shouldFinalize = claimFinalizationIfReady(state);
155
146
  waitStates.set(sessionId, states);
156
- logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Completion delivered=${state.deliveredCompletions}/${state.expectedCompletions}, complete=${complete}, shouldFinalize=${shouldFinalize}`);
147
+ logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Subagent ended, delivered=${state.deliveredCompletions}/${state.expectedCompletions}, complete=${complete}, shouldFinalize=${shouldFinalize}`);
157
148
  return { state, isComplete: complete, shouldFinalize };
158
149
  }
159
150
  /**
@@ -174,6 +165,18 @@ export function markParentSettled(sessionId, taskId) {
174
165
  logger.withContext(sessionId, taskId).log(`[SUBAGENT-WAIT] Parent settled, completions=${state.deliveredCompletions}/${state.expectedCompletions}, shouldFinalize=${shouldFinalize}`);
175
166
  return { state, isComplete: complete, shouldFinalize };
176
167
  }
168
+ /**
169
+ * Store a completion text snippet captured from xyOutbound.sendText.
170
+ * Does NOT increment delivery count (that's done by markSubagentEnded).
171
+ */
172
+ export function addCompletionText(sessionId, taskId, text) {
173
+ const states = getStatesArray(sessionId);
174
+ const state = states.find((s) => s.taskId === taskId);
175
+ if (!state || !text.trim())
176
+ return;
177
+ state.completionTexts.push(text.trim());
178
+ waitStates.set(sessionId, states);
179
+ }
177
180
  export function attachHeartbeat(sessionId, taskId, stopHeartbeat) {
178
181
  const states = getStatesArray(sessionId);
179
182
  const state = states.find((s) => s.taskId === taskId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.189-next",
3
+ "version": "0.0.190-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",