@xgjktech/xg_cwork_im 1.19.35 → 1.19.37
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.d.ts.map +1 -1
- package/dist/index.js +24 -16
- package/dist/index.js.map +1 -1
- package/dist/src/inbound/dispatch-mentioned-reply.d.ts.map +1 -1
- package/dist/src/inbound/dispatch-mentioned-reply.js +3 -1
- package/dist/src/inbound/dispatch-mentioned-reply.js.map +1 -1
- package/dist/src/plan/plan-core-adapter.d.ts +6 -3
- package/dist/src/plan/plan-core-adapter.d.ts.map +1 -1
- package/dist/src/plan/plan-core-adapter.js +12 -6
- package/dist/src/plan/plan-core-adapter.js.map +1 -1
- package/dist/src/plan/plan-core-adapter.test.js +22 -3
- package/dist/src/plan/plan-core-adapter.test.js.map +1 -1
- package/dist/src/plan/plan-core-db.d.ts.map +1 -1
- package/dist/src/plan/plan-core-db.js +4 -2
- package/dist/src/plan/plan-core-db.js.map +1 -1
- package/dist/src/plan/plan-core-db.test.js +19 -1
- package/dist/src/plan/plan-core-db.test.js.map +1 -1
- package/dist/src/plan/plan-core-snapshot.d.ts.map +1 -1
- package/dist/src/plan/plan-core-snapshot.js +10 -2
- package/dist/src/plan/plan-core-snapshot.js.map +1 -1
- package/dist/src/plan/plan-core-snapshot.test.js +24 -0
- package/dist/src/plan/plan-core-snapshot.test.js.map +1 -1
- package/dist/src/turn/interrupt-round.test.js +23 -17
- package/dist/src/turn/interrupt-round.test.js.map +1 -1
- package/dist/src/turn/subagent-monitor.d.ts +33 -17
- package/dist/src/turn/subagent-monitor.d.ts.map +1 -1
- package/dist/src/turn/subagent-monitor.js +275 -229
- package/dist/src/turn/subagent-monitor.js.map +1 -1
- package/dist/src/turn/subagent-monitor.test.js +111 -26
- package/dist/src/turn/subagent-monitor.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { getOrCreateShared, getSharedMap } from "../plugin/process-shared-store.js";
|
|
2
2
|
import { turnOnAllChildrenDone } from "./turn-end-controller.js";
|
|
3
|
-
/** 模块实例 ID:用于确认插件是否被重复加载(若日志出现两个不同 id
|
|
3
|
+
/** 模块实例 ID:用于确认插件是否被重复加载(若日志出现两个不同 id,则存在双实例)。 */
|
|
4
4
|
const MODULE_INSTANCE_ID = `m${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
5
5
|
console.log(`[subagent-monitor] module loaded instance=${MODULE_INSTANCE_ID} pid=${process.pid} title=${(process.title || "(none)").trim()}`);
|
|
6
|
-
//
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const deferredFinished = getSharedMap("subagent-deferred-finished");
|
|
6
|
+
// 进程级共享:跨模块重载后状态不中断。
|
|
7
|
+
const targets = getSharedMap("subagent-targets");
|
|
8
|
+
const groupAnchors = getSharedMap("subagent-group-anchors");
|
|
10
9
|
const roundAdmissionTombstones = getSharedMap("subagent-round-admission-tombstones");
|
|
11
10
|
/** 记录被 `/stop` 中断的 subagent run(按 childSessionKey 索引,存被中断 runId 与过期时间)。 */
|
|
12
11
|
const childTombstones = getSharedMap("subagent-child-tombstones");
|
|
13
12
|
const POLL_INTERVAL_MS = 15_000;
|
|
14
13
|
/**
|
|
15
14
|
* apiCandidates 保留上限:OpenClaw 会对同一模块反复重注册(多代 registry 并存且从不触发
|
|
16
|
-
* cleanup
|
|
17
|
-
*
|
|
18
|
-
* 不可用时的回退池」(resolveUsablePluginApi),而单进程内 register 是按时间顺序的短爆发(通常
|
|
19
|
-
* 2~4 次),功能上只需最近几代即可,故截断更早的候选。
|
|
15
|
+
* cleanup)。数组仅作为「当前 runtime 探测不可用时的回退池」(resolveUsablePluginApi),
|
|
16
|
+
* 单进程内 register 是按时间顺序的短爆发,功能上只需最近几代即可,故截断更早候选。
|
|
20
17
|
*/
|
|
21
18
|
const MAX_API_CANDIDATES = 4;
|
|
22
19
|
const ROUND_TOMBSTONE_TTL_MS = 30_000;
|
|
23
20
|
const CHILD_TOMBSTONE_TTL_MS = 5 * 60_000;
|
|
21
|
+
const GROUP_ANCHOR_TTL_MS = 24 * 60 * 60_000;
|
|
22
|
+
/** 仍在跑的目标最长存活:仅作泄漏兜底;正常由 ended 事件 / 轮询对账回收。 */
|
|
23
|
+
const TARGET_TTL_MS = 60 * 60_000;
|
|
24
|
+
/** 已完成目标的保留窗口:用于 /stop 快照与迟到事件幂等去重,随后回收。 */
|
|
25
|
+
const COMPLETED_TARGET_TTL_MS = 5 * 60_000;
|
|
24
26
|
const MAX_INCR_MSGS = 5;
|
|
25
27
|
const MSG_TRUNCATE_LEN = 40;
|
|
26
28
|
/** 单次 tick 用 openclaw `agent.wait` 对账 subagent run 状态的探测超时。 */
|
|
@@ -111,7 +113,15 @@ async function resolveUsablePluginApi() {
|
|
|
111
113
|
export function getSubagentMonitorInstanceId() {
|
|
112
114
|
return MODULE_INSTANCE_ID;
|
|
113
115
|
}
|
|
114
|
-
|
|
116
|
+
/** 记录该会话最近一次消息锚点,供拿不到 `requester.messageId` 时兜底。 */
|
|
117
|
+
export function noteSubagentGroupAnchor(groupId, anchor) {
|
|
118
|
+
const gid = groupId?.trim();
|
|
119
|
+
const a = anchor?.trim();
|
|
120
|
+
if (!gid || !a)
|
|
121
|
+
return;
|
|
122
|
+
groupAnchors.set(gid, { anchor: a, expiresAt: Date.now() + GROUP_ANCHOR_TTL_MS });
|
|
123
|
+
}
|
|
124
|
+
function prune(now = Date.now()) {
|
|
115
125
|
for (const [key, expiresAt] of roundAdmissionTombstones) {
|
|
116
126
|
if (expiresAt <= now)
|
|
117
127
|
roundAdmissionTombstones.delete(key);
|
|
@@ -120,270 +130,304 @@ function pruneTombstones(now = Date.now()) {
|
|
|
120
130
|
if (tombstone.expiresAt <= now)
|
|
121
131
|
childTombstones.delete(key);
|
|
122
132
|
}
|
|
133
|
+
for (const [key, entry] of groupAnchors) {
|
|
134
|
+
if (entry.expiresAt <= now)
|
|
135
|
+
groupAnchors.delete(key);
|
|
136
|
+
}
|
|
137
|
+
for (const [key, target] of Array.from(targets)) {
|
|
138
|
+
if (target.expiresAt <= now) {
|
|
139
|
+
console.warn(`[subagent-monitor] target TTL expired instance=${MODULE_INSTANCE_ID} ` +
|
|
140
|
+
`childSessionKey=${target.childSessionKey} runId=${target.runId} completed=${target.completed} completedSent=${target.completedStatSent === true}`);
|
|
141
|
+
targets.delete(key);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// 已完成且终态快照已推送的目标,保留一小段窗口(/stop 快照 + 迟到事件去重)后回收;
|
|
145
|
+
// 已完成但快照尚未推送成功的目标不得回收,留给 tick 重试。
|
|
146
|
+
if (target.completed &&
|
|
147
|
+
target.completedStatSent &&
|
|
148
|
+
(target.completedAt ?? 0) + COMPLETED_TARGET_TTL_MS <= now) {
|
|
149
|
+
targets.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** 是否仍有待处理目标:仍在跑,或已完成但终态快照尚未成功推送。 */
|
|
154
|
+
function hasPendingTarget() {
|
|
155
|
+
for (const target of targets.values()) {
|
|
156
|
+
if (!target.completed || !target.completedStatSent)
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
123
160
|
}
|
|
124
161
|
function stopTickerIfIdle() {
|
|
125
|
-
if (
|
|
162
|
+
if (!hasPendingTarget() && m.ticker) {
|
|
126
163
|
clearInterval(m.ticker);
|
|
127
164
|
m.ticker = null;
|
|
128
165
|
}
|
|
129
166
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (deferredFinished.get(monitor.roundKey)?.generation === monitor.generation) {
|
|
137
|
-
deferredFinished.delete(monitor.roundKey);
|
|
138
|
-
}
|
|
139
|
-
for (const record of monitor.subagents.values()) {
|
|
140
|
-
const indexed = childToRound.get(record.childSessionKey);
|
|
141
|
-
if (indexed?.roundKey === monitor.roundKey && indexed.generation === monitor.generation) {
|
|
142
|
-
childToRound.delete(record.childSessionKey);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
stopTickerIfIdle();
|
|
167
|
+
function ensureTickerRunning() {
|
|
168
|
+
if (m.ticker)
|
|
169
|
+
return;
|
|
170
|
+
void tick();
|
|
171
|
+
m.ticker = setInterval(() => void tick(), POLL_INTERVAL_MS);
|
|
172
|
+
m.ticker.unref?.();
|
|
146
173
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
/**
|
|
175
|
+
* 登记一个子任务的发送目标并立即推送 running。
|
|
176
|
+
*
|
|
177
|
+
* 不要求存在活跃轮次:`roundKey`/`generation` 缺省时仅做发送与对账,不参与整轮计数。
|
|
178
|
+
*/
|
|
179
|
+
export function registerSubagent(params) {
|
|
180
|
+
prune();
|
|
181
|
+
if (!params.childSessionKey || !params.runId) {
|
|
182
|
+
console.warn(`[subagent-monitor] register rejected instance=${MODULE_INSTANCE_ID} reason=missingKey`);
|
|
151
183
|
return false;
|
|
152
184
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
monitor = undefined;
|
|
157
|
-
}
|
|
158
|
-
if (!monitor) {
|
|
159
|
-
monitor = {
|
|
160
|
-
roundKey: params.roundKey,
|
|
161
|
-
generation: params.generation,
|
|
162
|
-
groupId: params.groupId,
|
|
163
|
-
agentId: params.agentId,
|
|
164
|
-
streamClient: params.streamClient,
|
|
165
|
-
replyToMsgId: params.replyToMsgId,
|
|
166
|
-
subagents: new Map(),
|
|
167
|
-
};
|
|
168
|
-
roundMonitors.set(params.roundKey, monitor);
|
|
169
|
-
}
|
|
170
|
-
if (monitor.subagents.has(params.spawn.runId)) {
|
|
171
|
-
console.warn(`[subagent-monitor] startMonitor rejected instance=${MODULE_INSTANCE_ID} reason=duplicateRunId roundKey=${params.roundKey} runId=${params.spawn.runId}`);
|
|
185
|
+
if (params.roundKey && roundAdmissionTombstones.has(params.roundKey)) {
|
|
186
|
+
console.warn(`[subagent-monitor] register rejected instance=${MODULE_INSTANCE_ID} reason=roundAdmissionTombstone ` +
|
|
187
|
+
`roundKey=${params.roundKey} runId=${params.runId}`);
|
|
172
188
|
return false;
|
|
173
189
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
const existing = targets.get(params.childSessionKey);
|
|
191
|
+
if (existing) {
|
|
192
|
+
if (existing.runId === params.runId) {
|
|
193
|
+
console.warn(`[subagent-monitor] register rejected instance=${MODULE_INSTANCE_ID} reason=duplicateRunId ` +
|
|
194
|
+
`childSessionKey=${params.childSessionKey} runId=${params.runId}`);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// 同 childSessionKey 的新 run(steer-restart / run 重建):覆盖旧目标。
|
|
198
|
+
// 旧 run 的卡片若不收尾会永久停在「进行中」,这里补发一次 completed 再移除。
|
|
199
|
+
console.warn(`[subagent-monitor] register replace instance=${MODULE_INSTANCE_ID} childSessionKey=${params.childSessionKey} ` +
|
|
200
|
+
`oldRunId=${existing.runId} newRunId=${params.runId}`);
|
|
201
|
+
void closeSupersededTarget(existing).catch((err) => {
|
|
202
|
+
console.error(`[subagent-monitor] close superseded target failed: ${String(err)}`);
|
|
203
|
+
});
|
|
204
|
+
targets.delete(params.childSessionKey);
|
|
205
|
+
}
|
|
206
|
+
const anchor = (params.replyToMsgId ?? "").trim() || groupAnchors.get(params.groupId)?.anchor || "";
|
|
207
|
+
if (anchor) {
|
|
208
|
+
noteSubagentGroupAnchor(params.groupId, anchor);
|
|
209
|
+
}
|
|
210
|
+
const target = {
|
|
211
|
+
childSessionKey: params.childSessionKey,
|
|
212
|
+
runId: params.runId,
|
|
213
|
+
label: params.label ?? params.runId.slice(0, 8),
|
|
214
|
+
agentId: params.agentId,
|
|
215
|
+
groupId: params.groupId,
|
|
216
|
+
anchorMsgId: anchor,
|
|
217
|
+
streamClient: params.streamClient,
|
|
178
218
|
messages: [],
|
|
219
|
+
lastMsgIndex: 0,
|
|
179
220
|
sendFrom: 0,
|
|
180
221
|
reading: false,
|
|
222
|
+
completed: false,
|
|
223
|
+
roundKey: params.roundKey,
|
|
224
|
+
generation: params.generation,
|
|
225
|
+
expiresAt: Date.now() + TARGET_TTL_MS,
|
|
181
226
|
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
roundKey
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
console.warn(`[subagent-monitor] startMonitor ok instance=${MODULE_INSTANCE_ID} roundKey=${monitor.roundKey} gen=${monitor.generation} ` +
|
|
189
|
-
`childSessionKey=${record.childSessionKey} runId=${record.runId}`);
|
|
190
|
-
sendWsStat(monitor, monitor.streamClient);
|
|
227
|
+
targets.set(target.childSessionKey, target);
|
|
228
|
+
console.log(`[subagent-monitor] register ok instance=${MODULE_INSTANCE_ID} childSessionKey=${target.childSessionKey} ` +
|
|
229
|
+
`runId=${target.runId} round=${target.roundKey ? `${target.roundKey}#${target.generation}` : "(none)"} ` +
|
|
230
|
+
`anchor=${anchor ? "yes" : "(none)"}`);
|
|
231
|
+
void pushSubagentStat(target);
|
|
191
232
|
ensureTickerRunning();
|
|
192
233
|
return true;
|
|
193
234
|
}
|
|
194
235
|
export function hasActiveSubagents(roundKey, generation) {
|
|
195
|
-
|
|
196
|
-
return Boolean(monitor &&
|
|
197
|
-
monitor.generation === generation &&
|
|
198
|
-
Array.from(monitor.subagents.values()).some((record) => !record.completed));
|
|
236
|
+
return Array.from(targets.values()).some((target) => target.roundKey === roundKey && target.generation === generation && !target.completed);
|
|
199
237
|
}
|
|
200
238
|
/** 子任务结束事件定位其所属轮次(供 turn-end-controller 递减活跃子任务数)。 */
|
|
201
239
|
export function findChildRoundIndex(childSessionKey) {
|
|
202
|
-
|
|
240
|
+
const target = targets.get(childSessionKey);
|
|
241
|
+
if (!target || target.roundKey === undefined || target.generation === undefined)
|
|
242
|
+
return undefined;
|
|
243
|
+
return { roundKey: target.roundKey, generation: target.generation, runId: target.runId };
|
|
203
244
|
}
|
|
204
|
-
|
|
205
|
-
|
|
245
|
+
/**
|
|
246
|
+
* 整轮收尾(dispatch wrapper finally)调用。
|
|
247
|
+
*
|
|
248
|
+
* 与旧实现不同:**不销毁仍在跑的目标**——仅解除其轮次归属(保留 groupId/锚点/client),
|
|
249
|
+
* 迟到完成仍能补发 completed;已完成的同类目标顺带回收。
|
|
250
|
+
*/
|
|
251
|
+
export function finalizeRoundMonitoring(roundKey, generation) {
|
|
252
|
+
for (const [key, target] of Array.from(targets)) {
|
|
253
|
+
if (target.roundKey !== roundKey || target.generation !== generation)
|
|
254
|
+
continue;
|
|
255
|
+
// 仅当 completed 快照已成功推送才回收;否则保留并解除轮次归属,交给 tick 重试补发。
|
|
256
|
+
if (target.completed && target.completedStatSent) {
|
|
257
|
+
targets.delete(key);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
target.roundKey = undefined;
|
|
261
|
+
target.generation = undefined;
|
|
262
|
+
}
|
|
206
263
|
}
|
|
207
264
|
export function prepareSubagentInterrupt(roundKey, generation, now = Date.now()) {
|
|
208
|
-
|
|
265
|
+
prune(now);
|
|
209
266
|
roundAdmissionTombstones.set(roundKey, now + ROUND_TOMBSTONE_TTL_MS);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
childTombstones.set(
|
|
215
|
-
runId:
|
|
267
|
+
const result = [];
|
|
268
|
+
for (const [key, target] of Array.from(targets)) {
|
|
269
|
+
if (target.roundKey !== roundKey || target.generation !== generation)
|
|
270
|
+
continue;
|
|
271
|
+
childTombstones.set(target.childSessionKey, {
|
|
272
|
+
runId: target.runId,
|
|
216
273
|
expiresAt: now + CHILD_TOMBSTONE_TTL_MS,
|
|
217
274
|
});
|
|
218
|
-
|
|
219
|
-
runId:
|
|
220
|
-
label:
|
|
221
|
-
status:
|
|
222
|
-
messages:
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
|
|
275
|
+
result.push({
|
|
276
|
+
runId: target.runId,
|
|
277
|
+
label: target.label,
|
|
278
|
+
status: target.completed ? "completed" : "suspended",
|
|
279
|
+
messages: target.messages,
|
|
280
|
+
});
|
|
281
|
+
targets.delete(key);
|
|
282
|
+
}
|
|
283
|
+
stopTickerIfIdle();
|
|
226
284
|
return result;
|
|
227
285
|
}
|
|
228
286
|
export function markSubagentCompleted(childSessionKey, runId) {
|
|
229
|
-
|
|
287
|
+
prune();
|
|
230
288
|
// 仅拦截被 `/stop` 中断的同一 runId 的迟到结束事件;新 run(不同 runId 复用同 key)不受影响。
|
|
231
289
|
const tombstone = childTombstones.get(childSessionKey);
|
|
232
290
|
if (tombstone && runId && runId === tombstone.runId)
|
|
233
291
|
return;
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
236
|
-
console.debug(`[subagent-monitor] markSubagentCompleted: no
|
|
292
|
+
const target = targets.get(childSessionKey);
|
|
293
|
+
if (!target) {
|
|
294
|
+
console.debug(`[subagent-monitor] markSubagentCompleted: no target instance=${MODULE_INSTANCE_ID} ` +
|
|
295
|
+
`childSessionKey=${childSessionKey} runId=${runId ?? "(none)"}`);
|
|
237
296
|
return;
|
|
238
297
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
console.debug(`[subagent-monitor] markSubagentCompleted: monitor mismatch childSessionKey=${childSessionKey} runId=${runId ?? "(none)"} gen=${entry.generation}`);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
// runId 可能与 spawn 时不同(steer-restart / run 重建):优先精确匹配,回退到 childSessionKey。
|
|
247
|
-
let record = runId ? monitor.subagents.get(runId) : undefined;
|
|
248
|
-
if (!record) {
|
|
249
|
-
record = Array.from(monitor.subagents.values()).find((r) => r.childSessionKey === childSessionKey);
|
|
250
|
-
if (record && runId && record.runId !== runId) {
|
|
251
|
-
console.debug(`[subagent-monitor] markSubagentCompleted: runId fallback childSessionKey=${childSessionKey} ` +
|
|
252
|
-
`eventRunId=${runId} recordRunId=${record.runId}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (!record) {
|
|
256
|
-
console.debug(`[subagent-monitor] markSubagentCompleted: no record childSessionKey=${childSessionKey} runId=${runId ?? "(none)"}`);
|
|
257
|
-
return;
|
|
298
|
+
if (runId && target.runId !== runId) {
|
|
299
|
+
// runId 可能与 spawn 时不同(steer-restart / run 重建):回退到 childSessionKey。
|
|
300
|
+
console.debug(`[subagent-monitor] markSubagentCompleted: runId fallback instance=${MODULE_INSTANCE_ID} ` +
|
|
301
|
+
`childSessionKey=${childSessionKey} eventRunId=${runId} targetRunId=${target.runId}`);
|
|
258
302
|
}
|
|
259
303
|
void (async () => {
|
|
260
304
|
try {
|
|
261
|
-
await readSubagentMessages(
|
|
305
|
+
await readSubagentMessages(target);
|
|
262
306
|
}
|
|
263
307
|
catch (err) {
|
|
264
308
|
console.error(`[subagent-monitor] completion read failed: ${String(err)}`);
|
|
265
309
|
}
|
|
266
|
-
|
|
267
|
-
return;
|
|
268
|
-
record.completed = true;
|
|
269
|
-
await settleMonitorIfComplete(monitor);
|
|
310
|
+
await completeTarget(target);
|
|
270
311
|
})();
|
|
271
312
|
}
|
|
272
|
-
async function
|
|
273
|
-
if (
|
|
274
|
-
return;
|
|
275
|
-
if (!Array.from(monitor.subagents.values()).every((record) => record.completed))
|
|
313
|
+
async function completeTarget(target) {
|
|
314
|
+
if (target.completed)
|
|
276
315
|
return;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return;
|
|
280
|
-
// 整轮收尾(WS FINISHED + RUN_USAGE + completeActiveRound)改由 turn-end-controller 在
|
|
281
|
-
// 主会话出现非 yield 段终态(或静默兜底)后,由 dispatch wrapper 统一执行;
|
|
282
|
-
// 这里只负责推送 SUBAGENT_STAT 快照,绝不自行 end/remove/complete ——
|
|
283
|
-
// OpenClaw 7.1+/8.x 中「所有已知子任务完成」不代表整轮结束(主会话可能 resume
|
|
284
|
-
// 继续派发新一批子任务),因此本快照允许多波次多次推送,仅对完全相同的状态去重。
|
|
316
|
+
target.completed = true;
|
|
317
|
+
target.completedAt = Date.now();
|
|
285
318
|
try {
|
|
286
|
-
await
|
|
287
|
-
for (const record of monitor.subagents.values()) {
|
|
288
|
-
record.sendFrom = record.messages.length;
|
|
289
|
-
}
|
|
290
|
-
monitor.lastPushedSignature = signature;
|
|
291
|
-
console.log(`[subagent-monitor] final SUBAGENT_STAT sent (settle) roundKey=${monitor.roundKey} gen=${monitor.generation} ` +
|
|
292
|
-
`subagents=${Array.from(monitor.subagents.values()).length} — waiting turn-end for FINISHED`);
|
|
319
|
+
await pushCompleted(target);
|
|
293
320
|
}
|
|
294
321
|
catch (err) {
|
|
295
|
-
console.error(`[subagent-monitor]
|
|
322
|
+
console.error(`[subagent-monitor] completed SUBAGENT_STAT failed: ${String(err)}`);
|
|
296
323
|
}
|
|
297
|
-
|
|
324
|
+
maybeAllChildrenDone(target.roundKey, target.generation);
|
|
325
|
+
stopTickerIfIdle();
|
|
298
326
|
}
|
|
299
327
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
328
|
+
* 覆盖同 childSessionKey 的旧 run 时,先把旧 run 的卡片收尾(补发 completed),
|
|
329
|
+
* 否则旧 runId 的卡片会永久停在「进行中」。不做整轮 activeChildren 对账:
|
|
330
|
+
* 该 child 的逻辑计数由紧接着登记的新目标继续代表。
|
|
302
331
|
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (!monitor || monitor.generation !== generation)
|
|
332
|
+
async function closeSupersededTarget(target) {
|
|
333
|
+
if (target.completed)
|
|
306
334
|
return;
|
|
307
|
-
|
|
335
|
+
target.completed = true;
|
|
336
|
+
target.completedAt = Date.now();
|
|
337
|
+
await pushCompleted(target);
|
|
308
338
|
}
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
/** 整轮计数对账:该轮已无未完成目标时,通知 turn-end-controller。 */
|
|
340
|
+
function maybeAllChildrenDone(roundKey, generation) {
|
|
341
|
+
if (!roundKey || generation === undefined)
|
|
311
342
|
return;
|
|
312
|
-
|
|
343
|
+
const anyActive = Array.from(targets.values()).some((target) => target.roundKey === roundKey && target.generation === generation && !target.completed);
|
|
344
|
+
if (!anyActive)
|
|
345
|
+
turnOnAllChildrenDone(roundKey, generation);
|
|
346
|
+
}
|
|
347
|
+
async function readSubagentMessages(target) {
|
|
348
|
+
if (!m.api || target.reading)
|
|
349
|
+
return;
|
|
350
|
+
target.reading = true;
|
|
313
351
|
try {
|
|
314
352
|
let messages = [];
|
|
315
353
|
try {
|
|
316
354
|
const result = await m.api.runtime.subagent.getSessionMessages({
|
|
317
|
-
sessionKey:
|
|
355
|
+
sessionKey: target.childSessionKey,
|
|
318
356
|
});
|
|
319
357
|
messages = Array.isArray(result.messages) ? result.messages : [];
|
|
320
358
|
}
|
|
321
359
|
catch (err) {
|
|
322
360
|
// runtime.subagent 可能只在 gateway 请求上下文内可用(openclaw 7.x request-scoped);
|
|
323
361
|
// 读取失败不能拖垮整个 tick,这里吞掉并继续,消息下次再拉。
|
|
324
|
-
console.warn(`[subagent-monitor] getSessionMessages failed childSessionKey=${
|
|
362
|
+
console.warn(`[subagent-monitor] getSessionMessages failed childSessionKey=${target.childSessionKey} runId=${target.runId}: ${String(err)}`);
|
|
325
363
|
}
|
|
326
|
-
for (const msg of messages.slice(
|
|
327
|
-
|
|
328
|
-
|
|
364
|
+
for (const msg of messages.slice(target.lastMsgIndex))
|
|
365
|
+
target.messages.push(cleanMsg(msg));
|
|
366
|
+
target.lastMsgIndex = messages.length;
|
|
329
367
|
}
|
|
330
368
|
finally {
|
|
331
|
-
|
|
369
|
+
target.reading = false;
|
|
332
370
|
}
|
|
333
371
|
}
|
|
334
372
|
/**
|
|
335
373
|
* 用 openclaw 权威 run 状态对账:run 已结束(ok/error)返回 true,仍在运行(timeout)返回 false。
|
|
336
374
|
* 探测失败(如 runId 已不存在、状态码异常)保守按「仍在运行」处理,交给事件路径兜底。
|
|
337
375
|
*/
|
|
338
|
-
async function probeRunStatus(
|
|
376
|
+
async function probeRunStatus(target) {
|
|
339
377
|
if (!m.api)
|
|
340
378
|
return false;
|
|
341
379
|
try {
|
|
342
380
|
const result = await m.api.runtime.subagent.waitForRun({
|
|
343
|
-
runId:
|
|
381
|
+
runId: target.runId,
|
|
344
382
|
timeoutMs: RUN_STATUS_PROBE_TIMEOUT_MS,
|
|
345
383
|
});
|
|
346
384
|
return result.status !== "timeout";
|
|
347
385
|
}
|
|
348
386
|
catch (err) {
|
|
349
|
-
console.warn(`[subagent-monitor] waitForRun probe failed runId=${
|
|
387
|
+
console.warn(`[subagent-monitor] waitForRun probe failed runId=${target.runId}: ${String(err)}`);
|
|
350
388
|
return false;
|
|
351
389
|
}
|
|
352
390
|
}
|
|
353
|
-
function ensureTickerRunning() {
|
|
354
|
-
if (m.ticker)
|
|
355
|
-
return;
|
|
356
|
-
void tick();
|
|
357
|
-
m.ticker = setInterval(() => void tick(), POLL_INTERVAL_MS);
|
|
358
|
-
m.ticker.unref?.();
|
|
359
|
-
}
|
|
360
391
|
async function tick() {
|
|
361
|
-
|
|
392
|
+
prune();
|
|
393
|
+
if (targets.size === 0) {
|
|
362
394
|
stopTickerIfIdle();
|
|
363
395
|
return;
|
|
364
396
|
}
|
|
365
397
|
// 确保使用可用的 runtime:探测并必要时切回候选中的可用 api,避免坏 runtime 继续抛 RequestScoped。
|
|
366
398
|
await resolveUsablePluginApi();
|
|
367
|
-
|
|
399
|
+
const running = Array.from(targets.values()).filter((target) => !target.completed);
|
|
400
|
+
// pending 含:仍在跑的目标,以及已完成但终态快照尚未成功推送的目标(需重试补发)。
|
|
401
|
+
const pending = Array.from(targets.values()).filter((target) => !target.completed || !target.completedStatSent);
|
|
402
|
+
try {
|
|
403
|
+
await Promise.all(running.map((target) => readSubagentMessages(target)));
|
|
404
|
+
// 并行对账:已结束的 run 在 gateway 侧有 terminal snapshot 缓存,立即返回;仍在跑的会阻塞至探测超时。
|
|
405
|
+
await Promise.all(running.map(async (target) => {
|
|
406
|
+
if (target.completed)
|
|
407
|
+
return;
|
|
408
|
+
if (await probeRunStatus(target)) {
|
|
409
|
+
target.completed = true;
|
|
410
|
+
target.completedAt = Date.now();
|
|
411
|
+
}
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
console.error(`[subagent-monitor] poll failed: ${String(err)}`);
|
|
416
|
+
}
|
|
417
|
+
for (const target of pending) {
|
|
418
|
+
if (targets.get(target.childSessionKey) !== target)
|
|
419
|
+
continue;
|
|
368
420
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return;
|
|
377
|
-
if (await probeRunStatus(record))
|
|
378
|
-
record.completed = true;
|
|
379
|
-
}));
|
|
380
|
-
if (roundMonitors.get(monitor.roundKey) !== monitor)
|
|
381
|
-
continue;
|
|
382
|
-
sendWsStat(monitor, monitor.streamClient);
|
|
383
|
-
await settleMonitorIfComplete(monitor);
|
|
421
|
+
if (target.completed) {
|
|
422
|
+
await pushCompleted(target);
|
|
423
|
+
maybeAllChildrenDone(target.roundKey, target.generation);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
await pushSubagentStat(target);
|
|
427
|
+
}
|
|
384
428
|
}
|
|
385
429
|
catch (err) {
|
|
386
|
-
console.error(`[subagent-monitor]
|
|
430
|
+
console.error(`[subagent-monitor] SUBAGENT_STAT failed childSessionKey=${target.childSessionKey}: ${String(err)}`);
|
|
387
431
|
}
|
|
388
432
|
}
|
|
389
433
|
stopTickerIfIdle();
|
|
@@ -421,59 +465,62 @@ function cleanMsg(msg) {
|
|
|
421
465
|
text = `${text.slice(0, MSG_TRUNCATE_LEN)}…`;
|
|
422
466
|
return `[助手] ${text}${toolCount > 0 ? ` [调用${toolCount}个工具]` : ""}`;
|
|
423
467
|
}
|
|
424
|
-
function toSubagentList(monitor, incremental) {
|
|
425
|
-
return Array.from(monitor.subagents.values()).map((record) => ({
|
|
426
|
-
runId: record.runId,
|
|
427
|
-
label: record.label ?? record.runId.slice(0, 8),
|
|
428
|
-
status: record.completed ? "completed" : "running",
|
|
429
|
-
messages: incremental
|
|
430
|
-
? record.messages.slice(record.sendFrom, record.sendFrom + MAX_INCR_MSGS)
|
|
431
|
-
: record.messages,
|
|
432
|
-
}));
|
|
433
|
-
}
|
|
434
468
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
469
|
+
* 推送一条 SUBAGENT_STAT(单目标、增量消息;状态取目标当前 completed)。
|
|
470
|
+
* 发送失败只记录、不抛出:fire-and-forget 调用点(spawn/tick)不应产生 unhandledRejection。
|
|
471
|
+
* 返回是否发送成功(未推进 sendFrom 时下次可重试)。
|
|
437
472
|
*/
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
.
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
subagentList.forEach((item) => {
|
|
460
|
-
const record = monitor.subagents.get(item.runId);
|
|
461
|
-
if (record)
|
|
462
|
-
record.sendFrom += item.messages.length;
|
|
473
|
+
async function pushSubagentStat(target) {
|
|
474
|
+
if (!target.anchorMsgId) {
|
|
475
|
+
console.debug(`[subagent-monitor] SUBAGENT_STAT skipped (no anchor) childSessionKey=${target.childSessionKey} runId=${target.runId}`);
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const messages = target.messages.slice(target.sendFrom, target.sendFrom + MAX_INCR_MSGS);
|
|
479
|
+
try {
|
|
480
|
+
await target.streamClient.sendEvent({
|
|
481
|
+
groupId: target.groupId,
|
|
482
|
+
replyToMsgId: target.anchorMsgId,
|
|
483
|
+
event: "SUBAGENT_STAT",
|
|
484
|
+
data: {
|
|
485
|
+
subagentList: [
|
|
486
|
+
{
|
|
487
|
+
runId: target.runId,
|
|
488
|
+
label: target.label,
|
|
489
|
+
status: target.completed ? "completed" : "running",
|
|
490
|
+
messages,
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
},
|
|
463
494
|
});
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
.
|
|
467
|
-
|
|
468
|
-
}
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
console.error(`[subagent-monitor] SUBAGENT_STAT failed childSessionKey=${target.childSessionKey} runId=${target.runId}: ${String(err)}`);
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
target.sendFrom += messages.length;
|
|
501
|
+
return true;
|
|
469
502
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
/** 完成收尾:补发全部待推消息,并至少推送一次 completed 状态(幂等;发送失败下次重试)。 */
|
|
504
|
+
async function pushCompleted(target) {
|
|
505
|
+
if (target.completedStatSent || target.completedStatSending)
|
|
506
|
+
return;
|
|
507
|
+
if (!target.anchorMsgId) {
|
|
508
|
+
// 无锚点无法发送(buildSendEventPayload 要求非空);标记已处理,避免无意义重试与 ticker 空转。
|
|
509
|
+
target.completedStatSent = true;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
target.completedStatSending = true;
|
|
513
|
+
try {
|
|
514
|
+
while (target.sendFrom < target.messages.length) {
|
|
515
|
+
if (!(await pushSubagentStat(target)))
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (await pushSubagentStat(target))
|
|
519
|
+
target.completedStatSent = true;
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
target.completedStatSending = false;
|
|
523
|
+
}
|
|
477
524
|
}
|
|
478
525
|
export function __tickForTests() {
|
|
479
526
|
return tick();
|
|
@@ -489,9 +536,8 @@ export function disposeSubagentMonitor() {
|
|
|
489
536
|
m.api = null;
|
|
490
537
|
m.apiProbe = "unverified";
|
|
491
538
|
m.apiCandidates.length = 0;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
deferredFinished.clear();
|
|
539
|
+
targets.clear();
|
|
540
|
+
groupAnchors.clear();
|
|
495
541
|
roundAdmissionTombstones.clear();
|
|
496
542
|
childTombstones.clear();
|
|
497
543
|
}
|