@ynhcj/xiaoyi-channel 0.0.192-next → 0.0.193-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 +0 -53
- package/dist/src/bot.js +1 -39
- package/dist/src/channel.js +19 -6
- package/dist/src/formatter.d.ts +0 -5
- package/dist/src/formatter.js +3 -3
- package/dist/src/outbound.d.ts +0 -7
- package/dist/src/outbound.js +5 -72
- package/dist/src/reply-dispatcher.js +0 -32
- package/dist/src/task-manager.d.ts +1 -9
- package/dist/src/task-manager.js +6 -49
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,8 +10,6 @@ import { getAllPushIds } from "./src/utils/pushid-manager.js";
|
|
|
10
10
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
11
11
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
12
12
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
13
|
-
import { markSubagentSpawned, markSubagentEnded, } from "./src/subagent-wait-state.js";
|
|
14
|
-
import { logger } from "./src/utils/logger.js";
|
|
15
13
|
/**
|
|
16
14
|
* Register the cron detection hook.
|
|
17
15
|
*
|
|
@@ -169,58 +167,7 @@ function readJobIdFromResult(result) {
|
|
|
169
167
|
}
|
|
170
168
|
return undefined;
|
|
171
169
|
}
|
|
172
|
-
function registerSubagentHooks(api) {
|
|
173
|
-
// subagent_spawned: fires after a subagent run is successfully registered.
|
|
174
|
-
// We increment the expected completion count so that onIdle knows to wait.
|
|
175
|
-
api.on("subagent_spawned", async (_event, ctx) => {
|
|
176
|
-
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
177
|
-
if (!requesterSessionKey)
|
|
178
|
-
return;
|
|
179
|
-
const count = markSubagentSpawned(requesterSessionKey);
|
|
180
|
-
if (count > 0) {
|
|
181
|
-
logger.log(`[XY-SUBAGENT] spawned, requesterSessionKey=${requesterSessionKey.slice(0, 30)}, expected=${count}`);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
// subagent_ended: fires when a subagent run terminates (complete/error/killed).
|
|
185
|
-
// This is the PRIMARY delivery tracking mechanism. When all expected
|
|
186
|
-
// subagents have ended and parent has settled, we finalize the A2A session.
|
|
187
|
-
api.on("subagent_ended", async (event, ctx) => {
|
|
188
|
-
try {
|
|
189
|
-
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
190
|
-
if (!requesterSessionKey) {
|
|
191
|
-
logger.log(`[XY-SUBAGENT-END] no requesterSessionKey in ctx`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const transition = markSubagentEnded(requesterSessionKey);
|
|
195
|
-
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}`);
|
|
196
|
-
if (transition?.shouldFinalize) {
|
|
197
|
-
logger.log(`[XY-SUBAGENT-END] Starting finalization...`);
|
|
198
|
-
const [{ resolveXYConfig }, { deliverSubagentFinalResult }, { getXYRuntime }] = await Promise.all([
|
|
199
|
-
import("./src/config.js"),
|
|
200
|
-
import("./src/outbound.js"),
|
|
201
|
-
import("./src/runtime.js"),
|
|
202
|
-
]);
|
|
203
|
-
const rt = getXYRuntime();
|
|
204
|
-
logger.log(`[XY-SUBAGENT-END] Runtime config type: ${typeof rt.config}, hasConfig=${!!rt.config}`);
|
|
205
|
-
const cfg = rt.config;
|
|
206
|
-
const config = resolveXYConfig(cfg);
|
|
207
|
-
logger.log(`[XY-SUBAGENT-END] XY config resolved, wsUrl=${config.wsUrl?.slice(0, 20)}`);
|
|
208
|
-
await deliverSubagentFinalResult({
|
|
209
|
-
config,
|
|
210
|
-
state: transition.state,
|
|
211
|
-
reason: "all-subagents-ended-after-parent-settled",
|
|
212
|
-
});
|
|
213
|
-
logger.log(`[XY-SUBAGENT-END] Finalized A2A session after all subagents ended`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
catch (err) {
|
|
217
|
-
logger.error(`[XY-SUBAGENT-END] Error in subagent_ended hook:`, err);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
170
|
function registerFullHooks(api) {
|
|
222
|
-
// SUBAGENT HOOKS: track subagent spawn/end lifecycle for session keep-alive
|
|
223
|
-
registerSubagentHooks(api);
|
|
224
171
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
225
172
|
const pluginConfig = api.pluginConfig || {};
|
|
226
173
|
const skillRetrieverConfig = normalizeToolRetrieverConfig({
|
package/dist/src/bot.js
CHANGED
|
@@ -16,7 +16,6 @@ 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, hasWaitState, markParentSettled, } from "./subagent-wait-state.js";
|
|
20
19
|
import { logger } from "./utils/logger.js";
|
|
21
20
|
/**
|
|
22
21
|
* Handle an incoming A2A message.
|
|
@@ -178,11 +177,6 @@ export async function handleXYMessage(params) {
|
|
|
178
177
|
},
|
|
179
178
|
});
|
|
180
179
|
log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
|
|
181
|
-
// Register sessionKey→A2A sessionId mapping for subagent hook translation.
|
|
182
|
-
// Hooks receive openclaw sessionKey (e.g. "agent:main:direct:xxx") but
|
|
183
|
-
// xy_channel operates on A2A sessionId. This bridge lets subagent_spawned/
|
|
184
|
-
// subagent_ended hooks find the correct wait state.
|
|
185
|
-
registerSessionKeyMapping(route.sessionKey, parsed.sessionId, parsed.taskId, parsed.messageId);
|
|
186
180
|
// Check for ACP runtime binding on this A2A conversation
|
|
187
181
|
const runtimeRoute = resolveRuntimeConversationBindingRoute({
|
|
188
182
|
route,
|
|
@@ -375,16 +369,6 @@ export async function handleXYMessage(params) {
|
|
|
375
369
|
const cleanup = () => {
|
|
376
370
|
if (cleaned)
|
|
377
371
|
return;
|
|
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);
|
|
383
|
-
if (pendingWait && pendingWait.deliveredCompletions < pendingWait.expectedCompletions) {
|
|
384
|
-
// Subagent wait active — skip cleanup, session stays alive
|
|
385
|
-
log.log(`[BOT] Cleanup suppressed — subagent wait active on session, taskId=${parsed.taskId}`);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
372
|
cleaned = true;
|
|
389
373
|
log.log(`[BOT] Cleanup started`);
|
|
390
374
|
streamingSignals.delete(parsed.sessionId);
|
|
@@ -424,35 +408,13 @@ export async function handleXYMessage(params) {
|
|
|
424
408
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
425
409
|
await core.channel.reply.withReplyDispatcher({
|
|
426
410
|
dispatcher,
|
|
427
|
-
onSettled:
|
|
411
|
+
onSettled: () => {
|
|
428
412
|
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
429
413
|
// 🔑 When steered, skip cleanup — the first message's dispatcher is still running
|
|
430
414
|
if (steerState.steered) {
|
|
431
415
|
log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
|
|
432
416
|
return;
|
|
433
417
|
}
|
|
434
|
-
// Subagent wait state: if parent has pending subagents, mark settled
|
|
435
|
-
// and defer finalization. Cleanup is suppressed so the session stays alive.
|
|
436
|
-
const pendingWait = getWaitState(parsed.sessionId, parsed.taskId);
|
|
437
|
-
if (pendingWait && !pendingWait.parentSettled) {
|
|
438
|
-
const transition = markParentSettled(parsed.sessionId, parsed.taskId);
|
|
439
|
-
if (transition?.shouldFinalize) {
|
|
440
|
-
// All completions arrived before parent settled → finalize now
|
|
441
|
-
const { deliverSubagentFinalResult } = await import("./outbound.js");
|
|
442
|
-
await deliverSubagentFinalResult({
|
|
443
|
-
config,
|
|
444
|
-
state: transition.state,
|
|
445
|
-
reason: "all-subagent-results-delivered-before-parent-settled",
|
|
446
|
-
});
|
|
447
|
-
streamingSignals.delete(parsed.sessionId);
|
|
448
|
-
log.log(`[BOT] Subagent wait complete on parent settled; final response delivered`);
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
log.log(`[BOT] Subagent wait active, preserving session context for completion`);
|
|
452
|
-
}
|
|
453
|
-
dispatcherUpdaters.delete(parsed.sessionId);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
418
|
// cleanup 已由 onIdleComplete 在 onIdle 的 finally 中执行。
|
|
457
419
|
// onSettled 不做任何清理(直接在这里清理会发生 race condition)。
|
|
458
420
|
dispatcherUpdaters.delete(parsed.sessionId);
|
package/dist/src/channel.js
CHANGED
|
@@ -126,14 +126,27 @@ export const xyPlugin = {
|
|
|
126
126
|
? { conversationId, matchPriority: 2 }
|
|
127
127
|
: null;
|
|
128
128
|
},
|
|
129
|
-
resolveCommandConversation: ({ accountId }) => {
|
|
129
|
+
resolveCommandConversation: ({ accountId, sessionKey }) => {
|
|
130
130
|
// xiaoyi-channel: the A2A sessionId IS the conversationId.
|
|
131
|
-
//
|
|
131
|
+
// 1. Prefer the current ALS session context (works when the command
|
|
132
|
+
// is processed inside an active A2A message-handling turn).
|
|
132
133
|
const ctx = getCurrentSessionContext();
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
return
|
|
136
|
-
|
|
134
|
+
const alsSessionId = ctx?.sessionId?.trim();
|
|
135
|
+
if (alsSessionId)
|
|
136
|
+
return { conversationId: alsSessionId };
|
|
137
|
+
// 2. Fall back to parsing the session key. For xiaoyi-channel the
|
|
138
|
+
// session key has the form:
|
|
139
|
+
// agent:<agentId>:xiaoyi-channel:<accountId>:<sessionId>
|
|
140
|
+
// The last `:`-delimited segment is the A2A sessionId.
|
|
141
|
+
if (sessionKey) {
|
|
142
|
+
const lastColon = sessionKey.lastIndexOf(":");
|
|
143
|
+
if (lastColon >= 0) {
|
|
144
|
+
const sessionId = sessionKey.slice(lastColon + 1).trim();
|
|
145
|
+
if (sessionId)
|
|
146
|
+
return { conversationId: sessionId };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
137
150
|
},
|
|
138
151
|
},
|
|
139
152
|
reload: {
|
package/dist/src/formatter.d.ts
CHANGED
|
@@ -10,9 +10,6 @@ export interface SendA2AResponseParams {
|
|
|
10
10
|
text?: string;
|
|
11
11
|
append: boolean;
|
|
12
12
|
final: boolean;
|
|
13
|
-
/** Optional: reuse an existing artifactId instead of generating a new one.
|
|
14
|
-
* Used by subagent completion to link results to the original artifact. */
|
|
15
|
-
artifactId?: string;
|
|
16
13
|
files?: Array<{
|
|
17
14
|
fileName: string;
|
|
18
15
|
fileType: string;
|
|
@@ -53,8 +50,6 @@ export interface SendStatusUpdateParams {
|
|
|
53
50
|
messageId: string;
|
|
54
51
|
text: string;
|
|
55
52
|
state: "submitted" | "working" | "input-required" | "completed" | "canceled" | "failed" | "unknown";
|
|
56
|
-
/** When false, use the exact taskId/messageId passed in. Default true (dynamic lookup). */
|
|
57
|
-
useLatestTask?: boolean;
|
|
58
53
|
}
|
|
59
54
|
/**
|
|
60
55
|
* Send an A2A task status update.
|
package/dist/src/formatter.js
CHANGED
|
@@ -45,7 +45,7 @@ function buildTextPreview(text) {
|
|
|
45
45
|
* Send an A2A artifact update response.
|
|
46
46
|
*/
|
|
47
47
|
export async function sendA2AResponse(params) {
|
|
48
|
-
const { config, sessionId, taskId, messageId, text, append, final,
|
|
48
|
+
const { config, sessionId, taskId, messageId, text, append, final, files, errorCode, errorMessage, log: shouldLog = true } = params;
|
|
49
49
|
const log = logger.withContext(sessionId, taskId);
|
|
50
50
|
// 审批桥接:将 OpenClaw 的审批提示翻译成用户友好的确认文案
|
|
51
51
|
const bridgedText = text === undefined ? text : rewriteOutboundApprovalText(sessionId, text);
|
|
@@ -57,7 +57,7 @@ export async function sendA2AResponse(params) {
|
|
|
57
57
|
lastChunk: true,
|
|
58
58
|
final,
|
|
59
59
|
artifact: {
|
|
60
|
-
artifactId:
|
|
60
|
+
artifactId: uuidv4(),
|
|
61
61
|
parts: [],
|
|
62
62
|
},
|
|
63
63
|
};
|
|
@@ -157,7 +157,7 @@ export async function sendReasoningTextUpdate(params) {
|
|
|
157
157
|
* Follows A2A protocol standard format with nested status object.
|
|
158
158
|
*/
|
|
159
159
|
export async function sendStatusUpdate(params) {
|
|
160
|
-
const { config, sessionId, taskId, messageId, text, state
|
|
160
|
+
const { config, sessionId, taskId, messageId, text, state } = params;
|
|
161
161
|
const log = logger.withContext(sessionId, taskId);
|
|
162
162
|
// 审批桥接和脱敏
|
|
163
163
|
const bridgedText = rewriteOutboundApprovalText(sessionId, text);
|
package/dist/src/outbound.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
type ChannelOutboundAdapter = any;
|
|
2
|
-
import { resolveXYConfig } from "./config.js";
|
|
3
|
-
export declare function deliverSubagentFinalResult(params: {
|
|
4
|
-
config: ReturnType<typeof resolveXYConfig>;
|
|
5
|
-
state: import("./subagent-wait-state.js").SubagentWaitState;
|
|
6
|
-
reason?: string;
|
|
7
|
-
text?: string;
|
|
8
|
-
}): Promise<void>;
|
|
9
2
|
/**
|
|
10
3
|
* Outbound adapter for sending messages from OpenClaw to XY.
|
|
11
4
|
* Uses Push service for direct message delivery.
|
package/dist/src/outbound.js
CHANGED
|
@@ -6,45 +6,11 @@ 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, addCompletionText, clearWaitState, } from "./subagent-wait-state.js";
|
|
10
|
-
import { decrementTaskIdRef } from "./task-manager.js";
|
|
11
|
-
import { sendA2AResponse, sendStatusUpdate } from "./formatter.js";
|
|
12
9
|
import { savePushData } from "./utils/pushdata-manager.js";
|
|
13
10
|
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
14
11
|
import { logger } from "./utils/logger.js";
|
|
15
12
|
// Special marker for default push delivery when no target is specified
|
|
16
13
|
const DEFAULT_PUSH_MARKER = "default";
|
|
17
|
-
// ─── Subagent completion delivery ──────────────────────────────
|
|
18
|
-
export async function deliverSubagentFinalResult(params) {
|
|
19
|
-
const { config, state, reason, text } = params;
|
|
20
|
-
const log = logger.withContext(state.sessionId, state.taskId);
|
|
21
|
-
const finalText = state.completionTexts.length > 0
|
|
22
|
-
? state.completionTexts.join("\n\n")
|
|
23
|
-
: (text || undefined) ?? "子任务已完成";
|
|
24
|
-
await sendStatusUpdate({
|
|
25
|
-
config,
|
|
26
|
-
sessionId: state.sessionId,
|
|
27
|
-
taskId: state.taskId,
|
|
28
|
-
messageId: state.messageId,
|
|
29
|
-
text: "任务处理已完成~",
|
|
30
|
-
state: "completed",
|
|
31
|
-
useLatestTask: false,
|
|
32
|
-
});
|
|
33
|
-
await sendA2AResponse({
|
|
34
|
-
config,
|
|
35
|
-
sessionId: state.sessionId,
|
|
36
|
-
taskId: state.taskId,
|
|
37
|
-
messageId: state.messageId,
|
|
38
|
-
text: finalText,
|
|
39
|
-
append: false,
|
|
40
|
-
final: true,
|
|
41
|
-
artifactId: state.artifactId,
|
|
42
|
-
});
|
|
43
|
-
clearWaitState(state.sessionId, reason ?? "all-subagent-results-delivered", state.taskId);
|
|
44
|
-
decrementTaskIdRef(state.sessionId, state.taskId);
|
|
45
|
-
log.log(`[xyOutbound] Subagent final delivered to original A2A task, reason=${reason}`);
|
|
46
|
-
}
|
|
47
|
-
// ─── MIME types ─────────────────────────────────────────────────
|
|
48
14
|
// File extension to MIME type mapping
|
|
49
15
|
const FILE_TYPE_TO_MIME_TYPE = {
|
|
50
16
|
txt: "text/plain",
|
|
@@ -106,28 +72,20 @@ export const xyOutbound = {
|
|
|
106
72
|
// If the target doesn't contain "::", try to enhance it with taskId from session context
|
|
107
73
|
if (!trimmedTo.includes("::")) {
|
|
108
74
|
logger.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
|
|
109
|
-
// Try
|
|
75
|
+
// Try to get the current session context
|
|
110
76
|
const sessionContext = getCurrentSessionContext();
|
|
111
77
|
if (sessionContext && sessionContext.sessionId === trimmedTo) {
|
|
112
78
|
const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
|
|
113
|
-
logger.log(`[xyOutbound.resolveTarget] Enhanced target
|
|
79
|
+
logger.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
|
|
114
80
|
return {
|
|
115
81
|
ok: true,
|
|
116
82
|
to: enhancedTarget,
|
|
117
83
|
};
|
|
118
84
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const enhancedTarget = `${waitState.sessionId}::${waitState.taskId}`;
|
|
123
|
-
logger.withContext(waitState.sessionId, waitState.taskId).log(`[xyOutbound.resolveTarget] Enhanced target from subagent wait state: ${enhancedTarget}`);
|
|
124
|
-
return {
|
|
125
|
-
ok: true,
|
|
126
|
-
to: enhancedTarget,
|
|
127
|
-
};
|
|
85
|
+
else {
|
|
86
|
+
logger.log(`[xyOutbound.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
|
|
87
|
+
// Still return the original target, but it may fail in sendMedia
|
|
128
88
|
}
|
|
129
|
-
logger.log(`[xyOutbound.resolveTarget] Could not find matching session context or wait state for "${trimmedTo}"`);
|
|
130
|
-
// Still return the original target, but it may fail in sendMedia
|
|
131
89
|
}
|
|
132
90
|
// Otherwise, use the provided target (either already in correct format or for sendText)
|
|
133
91
|
logger.log(`[xyOutbound.resolveTarget] Using provided target:`, trimmedTo);
|
|
@@ -139,31 +97,6 @@ export const xyOutbound = {
|
|
|
139
97
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
140
98
|
// Resolve configuration
|
|
141
99
|
const config = resolveXYConfig(cfg);
|
|
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).
|
|
147
|
-
if (to && to !== DEFAULT_PUSH_MARKER) {
|
|
148
|
-
const [targetSessionId, targetTaskId] = String(to).split("::");
|
|
149
|
-
const waitState = getWaitState(targetSessionId, targetTaskId);
|
|
150
|
-
const alsCtx = getCurrentSessionContext();
|
|
151
|
-
const isFromWaitState = waitState &&
|
|
152
|
-
(!alsCtx || alsCtx.taskId !== targetTaskId);
|
|
153
|
-
if (isFromWaitState && !waitState.finalizationClaimed) {
|
|
154
|
-
const log = logger.withContext(waitState.sessionId, waitState.taskId);
|
|
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
|
|
159
|
-
return {
|
|
160
|
-
channel: "xiaoyi-channel",
|
|
161
|
-
messageId: waitState.artifactId,
|
|
162
|
-
chatId: to,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// ── End subagent text capture ───────────────────────────────
|
|
167
100
|
// Handle default push marker (for cron jobs without explicit target)
|
|
168
101
|
let actualTo = to;
|
|
169
102
|
if (to === DEFAULT_PUSH_MARKER) {
|
|
@@ -2,7 +2,6 @@ import { getXYRuntime } from "./runtime.js";
|
|
|
2
2
|
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate, sendCommand } from "./formatter.js";
|
|
3
3
|
import { resolveXYConfig } from "./config.js";
|
|
4
4
|
import { clearRunCrossTaskSentFiles, getCurrentSessionContext } from "./tools/session-manager.js";
|
|
5
|
-
import { getWaitState, clearWaitState, attachHeartbeat, } from "./subagent-wait-state.js";
|
|
6
5
|
import fs from "fs/promises";
|
|
7
6
|
import path from "path";
|
|
8
7
|
import { logger } from "./utils/logger.js";
|
|
@@ -233,34 +232,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
233
232
|
stopStatusInterval();
|
|
234
233
|
return;
|
|
235
234
|
}
|
|
236
|
-
// ── Subagent wait state check ─────────────────────────────
|
|
237
|
-
// If this session has pending subagent completions, suppress final:true
|
|
238
|
-
// and keep the A2A session alive. The final response will be sent when
|
|
239
|
-
// all completions arrive via xyOutbound.sendText interception.
|
|
240
|
-
const waitState = getWaitState(sessionId, taskId);
|
|
241
|
-
if (waitState &&
|
|
242
|
-
waitState.deliveredCompletions < waitState.expectedCompletions &&
|
|
243
|
-
!finalSent) {
|
|
244
|
-
scopedLog().log(`[ON-IDLE] Waiting for subagent completions ${waitState.deliveredCompletions}/${waitState.expectedCompletions}, suppressing final:true`);
|
|
245
|
-
try {
|
|
246
|
-
await sendStatusUpdate({
|
|
247
|
-
config,
|
|
248
|
-
sessionId,
|
|
249
|
-
taskId,
|
|
250
|
-
messageId,
|
|
251
|
-
text: "子任务正在处理中,请稍候~",
|
|
252
|
-
state: "working",
|
|
253
|
-
useLatestTask: false,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
catch (err) {
|
|
257
|
-
scopedLog().error(`[ON-IDLE] Failed to send subagent waiting status:`, err);
|
|
258
|
-
}
|
|
259
|
-
// Attach heartbeat so the status interval stays alive during wait
|
|
260
|
-
attachHeartbeat(sessionId, taskId, stopStatusInterval);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
// ── End subagent wait state check ─────────────────────────
|
|
264
235
|
// 🔑 用 try/finally 确保 cleanup 在 onIdle 的 async 工作全部完成后才执行。
|
|
265
236
|
// openclaw 的 waitForIdle() 以 void options.onIdle?.() 调用 onIdle,
|
|
266
237
|
// 不会 await 返回的 Promise,因此 onSettled 可能在 onIdle 中途触发。
|
|
@@ -307,9 +278,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
307
278
|
final: true,
|
|
308
279
|
});
|
|
309
280
|
finalSent = true;
|
|
310
|
-
if (waitState) {
|
|
311
|
-
clearWaitState(sessionId, "main-final-delivered", taskId);
|
|
312
|
-
}
|
|
313
281
|
scopedLog().log(`[ON-IDLE] Sent final response (empty, stream end)`);
|
|
314
282
|
}
|
|
315
283
|
catch (err) {
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
interface TaskIdEntry {
|
|
2
|
-
taskId: string;
|
|
3
|
-
messageId: string;
|
|
4
|
-
updatedAt: number;
|
|
5
|
-
}
|
|
6
1
|
interface TaskIdBinding {
|
|
7
2
|
sessionId: string;
|
|
8
3
|
currentTaskId: string;
|
|
9
4
|
currentMessageId: string;
|
|
10
5
|
updatedAt: number;
|
|
11
|
-
tasks?: TaskIdEntry[];
|
|
12
6
|
}
|
|
13
7
|
/**
|
|
14
8
|
* 注册或更新session的活跃taskId。
|
|
@@ -17,10 +11,8 @@ interface TaskIdBinding {
|
|
|
17
11
|
export declare function registerTaskId(sessionId: string, taskId: string, messageId: string): boolean;
|
|
18
12
|
/**
|
|
19
13
|
* 移除session的活跃taskId(消息处理完成时调用)。
|
|
20
|
-
* @param expectedTaskId 可选:精确移除指定的 taskId,而非整个 session 绑定。
|
|
21
|
-
* 用于 subagent completion 等需要精确清理特定 task 的场景。
|
|
22
14
|
*/
|
|
23
|
-
export declare function decrementTaskIdRef(sessionId: string
|
|
15
|
+
export declare function decrementTaskIdRef(sessionId: string): void;
|
|
24
16
|
/**
|
|
25
17
|
* 获取session的当前活跃taskId。
|
|
26
18
|
* 仅供 reply-dispatcher 跨链读取 steer 更新后的 taskId。
|
package/dist/src/task-manager.js
CHANGED
|
@@ -12,27 +12,6 @@ if (!_g.__xyActiveTaskIds) {
|
|
|
12
12
|
_g.__xyActiveTaskIds = new Map();
|
|
13
13
|
}
|
|
14
14
|
const activeTaskIds = _g.__xyActiveTaskIds;
|
|
15
|
-
function normalizeTasks(binding) {
|
|
16
|
-
if (Array.isArray(binding.tasks) && binding.tasks.length > 0) {
|
|
17
|
-
return binding.tasks;
|
|
18
|
-
}
|
|
19
|
-
return [{
|
|
20
|
-
taskId: binding.currentTaskId,
|
|
21
|
-
messageId: binding.currentMessageId,
|
|
22
|
-
updatedAt: binding.updatedAt,
|
|
23
|
-
}];
|
|
24
|
-
}
|
|
25
|
-
function syncCurrent(binding, tasks) {
|
|
26
|
-
binding.tasks = tasks;
|
|
27
|
-
const current = tasks[tasks.length - 1];
|
|
28
|
-
if (!current) {
|
|
29
|
-
activeTaskIds.delete(binding.sessionId);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
binding.currentTaskId = current.taskId;
|
|
33
|
-
binding.currentMessageId = current.messageId;
|
|
34
|
-
binding.updatedAt = current.updatedAt;
|
|
35
|
-
}
|
|
36
15
|
/**
|
|
37
16
|
* 注册或更新session的活跃taskId。
|
|
38
17
|
* Returns true if this was an update (session already had an active task).
|
|
@@ -41,9 +20,9 @@ export function registerTaskId(sessionId, taskId, messageId) {
|
|
|
41
20
|
const existing = activeTaskIds.get(sessionId);
|
|
42
21
|
if (existing) {
|
|
43
22
|
logger.log(`[TASK_MANAGER] Updating taskId: ${existing.currentTaskId} → ${taskId}`);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
23
|
+
existing.currentTaskId = taskId;
|
|
24
|
+
existing.currentMessageId = messageId;
|
|
25
|
+
existing.updatedAt = Date.now();
|
|
47
26
|
return true; // isUpdate
|
|
48
27
|
}
|
|
49
28
|
else {
|
|
@@ -52,7 +31,6 @@ export function registerTaskId(sessionId, taskId, messageId) {
|
|
|
52
31
|
currentTaskId: taskId,
|
|
53
32
|
currentMessageId: messageId,
|
|
54
33
|
updatedAt: Date.now(),
|
|
55
|
-
tasks: [{ taskId, messageId, updatedAt: Date.now() }],
|
|
56
34
|
});
|
|
57
35
|
logger.log(`[TASK_MANAGER] Registered new taskId: ${taskId}`);
|
|
58
36
|
return false;
|
|
@@ -60,31 +38,10 @@ export function registerTaskId(sessionId, taskId, messageId) {
|
|
|
60
38
|
}
|
|
61
39
|
/**
|
|
62
40
|
* 移除session的活跃taskId(消息处理完成时调用)。
|
|
63
|
-
* @param expectedTaskId 可选:精确移除指定的 taskId,而非整个 session 绑定。
|
|
64
|
-
* 用于 subagent completion 等需要精确清理特定 task 的场景。
|
|
65
41
|
*/
|
|
66
|
-
export function decrementTaskIdRef(sessionId
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
activeTaskIds.delete(sessionId);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const existing = activeTaskIds.get(sessionId);
|
|
73
|
-
if (!existing)
|
|
74
|
-
return;
|
|
75
|
-
const tasks = normalizeTasks(existing);
|
|
76
|
-
const nextTasks = tasks.filter((entry) => entry.taskId !== expectedTaskId);
|
|
77
|
-
if (nextTasks.length === tasks.length) {
|
|
78
|
-
logger.log(`[TASK_MANAGER] Preserving taskId ${existing.currentTaskId}; completed task ${expectedTaskId} is not active`);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
syncCurrent(existing, nextTasks);
|
|
82
|
-
if (nextTasks.length > 0) {
|
|
83
|
-
logger.log(`[TASK_MANAGER] Removed taskId ${expectedTaskId}, restored current taskId ${existing.currentTaskId}`);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
logger.log(`[TASK_MANAGER] Removing taskId (last task ${expectedTaskId})`);
|
|
87
|
-
}
|
|
42
|
+
export function decrementTaskIdRef(sessionId) {
|
|
43
|
+
logger.log(`[TASK_MANAGER] Removing taskId`);
|
|
44
|
+
activeTaskIds.delete(sessionId);
|
|
88
45
|
}
|
|
89
46
|
/**
|
|
90
47
|
* 获取session的当前活跃taskId。
|