@teamclaws/teamclaw 2026.3.25 → 2026.3.26-1
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/README.md +6 -0
- package/cli.mjs +519 -28
- package/index.ts +31 -16
- package/openclaw.plugin.json +3 -3
- package/package.json +16 -4
- package/src/config.ts +2 -2
- package/src/controller/controller-capacity.ts +23 -0
- package/src/controller/controller-service.ts +27 -1
- package/src/controller/controller-tools.ts +251 -7
- package/src/controller/http-server.ts +976 -38
- package/src/controller/orchestration-manifest.ts +105 -0
- package/src/controller/prompt-injector.ts +42 -7
- package/src/controller/worker-provisioning.ts +171 -13
- package/src/git-collaboration.ts +2 -0
- package/src/interaction-contracts.ts +459 -0
- package/src/task-executor.ts +482 -33
- package/src/types.ts +96 -0
- package/src/ui/app.js +313 -8
- package/src/ui/style.css +152 -0
- package/src/worker/http-handler.ts +15 -7
- package/src/worker/prompt-injector.ts +2 -0
- package/src/worker/skill-installer.ts +13 -0
- package/src/worker/tools.ts +172 -3
- package/src/worker/worker-service.ts +9 -6
package/src/task-executor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, PluginLogger } from "../api.js";
|
|
2
2
|
import { getRole } from "./roles.js";
|
|
3
|
-
import type { RoleId, TaskExecutionEventInput } from "./types.js";
|
|
3
|
+
import type { RoleId, TaskAssignmentPayload, TaskExecutionEventInput } from "./types.js";
|
|
4
4
|
|
|
5
5
|
const TEAMCLAW_ROLE_IDS_TEXT = [
|
|
6
6
|
"pm",
|
|
@@ -18,6 +18,13 @@ const TEAMCLAW_ROLE_IDS_TEXT = [
|
|
|
18
18
|
const SESSION_PROGRESS_POLL_INTERVAL_MS = 1000;
|
|
19
19
|
const SESSION_PROGRESS_MESSAGE_LIMIT = 200;
|
|
20
20
|
const MAX_SESSION_PROGRESS_MESSAGE_CHARS = 4000;
|
|
21
|
+
const RUN_WAIT_SLICE_MS = 30_000;
|
|
22
|
+
const RATE_LIMIT_STALL_PROBE_MS = 5 * 60 * 1000;
|
|
23
|
+
const RATE_LIMIT_PROBE_TIMEOUT_MS = 60_000;
|
|
24
|
+
const BACKGROUND_WORK_PROBE_MS = 60_000;
|
|
25
|
+
const BACKGROUND_WORK_PROBE_TIMEOUT_MS = 60_000;
|
|
26
|
+
const CHILD_SESSION_PROGRESS_POLL_INTERVAL_MS = 5_000;
|
|
27
|
+
const RATE_LIMIT_WAITING_SENTINEL = "TEAMCLAW_STILL_WAITING";
|
|
21
28
|
const TOOL_CALL_BLOCK_TYPES = new Set(["tool_use", "toolcall", "tool_call"]);
|
|
22
29
|
const TOOL_RESULT_BLOCK_TYPES = new Set(["tool_result", "tool_result_error"]);
|
|
23
30
|
|
|
@@ -26,21 +33,32 @@ type SessionProgressEntry = {
|
|
|
26
33
|
message: string;
|
|
27
34
|
phase: string;
|
|
28
35
|
stream: string;
|
|
36
|
+
isRateLimit: boolean;
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
type SessionProgressSnapshot = {
|
|
32
40
|
fingerprints: string[];
|
|
41
|
+
childSessionKeys: string[];
|
|
42
|
+
childFingerprints: Map<string, string[]>;
|
|
43
|
+
lastChildPollAt: number;
|
|
33
44
|
lastAssistantMessage: string;
|
|
34
45
|
latestMessages: unknown[];
|
|
35
46
|
};
|
|
36
47
|
|
|
48
|
+
type AssistantTurnSnapshot = {
|
|
49
|
+
text: string;
|
|
50
|
+
toolCalls: string[];
|
|
51
|
+
yielded: boolean;
|
|
52
|
+
backgroundPending: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
37
55
|
export type RoleTaskExecutorDeps = {
|
|
38
56
|
runtime: OpenClawPluginApi["runtime"];
|
|
39
57
|
logger: PluginLogger;
|
|
40
58
|
role: RoleId;
|
|
41
59
|
taskTimeoutMs: number;
|
|
42
|
-
getSessionKey: (
|
|
43
|
-
getIdempotencyKey?: (
|
|
60
|
+
getSessionKey: (assignment: TaskAssignmentPayload) => string;
|
|
61
|
+
getIdempotencyKey?: (assignment: TaskAssignmentPayload) => string;
|
|
44
62
|
reportExecutionEvent?: (taskId: string, event: TaskExecutionEventInput) => Promise<void> | void;
|
|
45
63
|
};
|
|
46
64
|
|
|
@@ -51,8 +69,10 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
51
69
|
? roleDef.systemPrompt
|
|
52
70
|
: `You are a ${role} in a virtual software team. Complete the assigned task.`;
|
|
53
71
|
|
|
54
|
-
return async (taskDescription: string,
|
|
55
|
-
const
|
|
72
|
+
return async (taskDescription: string, assignment: TaskAssignmentPayload): Promise<string> => {
|
|
73
|
+
const taskId = assignment.taskId;
|
|
74
|
+
const sessionKey = getSessionKey(assignment);
|
|
75
|
+
const idempotencyKey = getIdempotencyKey?.(assignment);
|
|
56
76
|
const taskMessage = buildTaskMessage(taskDescription, taskId, roleDef?.label ?? role);
|
|
57
77
|
logger.info(`TeamClaw: executing task ${taskId} as ${role} via subagent`);
|
|
58
78
|
|
|
@@ -76,7 +96,7 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
76
96
|
sessionKey,
|
|
77
97
|
message: taskMessage,
|
|
78
98
|
extraSystemPrompt: roleSystemPrompt,
|
|
79
|
-
idempotencyKey
|
|
99
|
+
idempotencyKey,
|
|
80
100
|
});
|
|
81
101
|
|
|
82
102
|
logger.info(`TeamClaw: subagent run started for task ${taskId}, runId=${runResult.runId}`);
|
|
@@ -92,9 +112,81 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
92
112
|
|
|
93
113
|
const progressSnapshot: SessionProgressSnapshot = {
|
|
94
114
|
fingerprints: [],
|
|
115
|
+
childSessionKeys: [],
|
|
116
|
+
childFingerprints: new Map(),
|
|
117
|
+
lastChildPollAt: 0,
|
|
95
118
|
lastAssistantMessage: "",
|
|
96
119
|
latestMessages: [],
|
|
97
120
|
};
|
|
121
|
+
const deadline = Date.now() + taskTimeoutMs;
|
|
122
|
+
const rateLimitState: {
|
|
123
|
+
active: boolean;
|
|
124
|
+
visibleAt?: number;
|
|
125
|
+
nextProbeAt?: number;
|
|
126
|
+
probeCount: number;
|
|
127
|
+
} = {
|
|
128
|
+
active: false,
|
|
129
|
+
probeCount: 0,
|
|
130
|
+
};
|
|
131
|
+
const backgroundWaitState: {
|
|
132
|
+
active: boolean;
|
|
133
|
+
visibleAt?: number;
|
|
134
|
+
nextProbeAt?: number;
|
|
135
|
+
probeCount: number;
|
|
136
|
+
} = {
|
|
137
|
+
active: false,
|
|
138
|
+
probeCount: 0,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const markRateLimitWaiting = async (): Promise<void> => {
|
|
142
|
+
if (rateLimitState.active) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
rateLimitState.active = true;
|
|
147
|
+
rateLimitState.visibleAt = now;
|
|
148
|
+
rateLimitState.nextProbeAt = now + RATE_LIMIT_STALL_PROBE_MS;
|
|
149
|
+
await emitExecutionEvent({
|
|
150
|
+
type: "progress",
|
|
151
|
+
phase: "model_rate_limit_waiting",
|
|
152
|
+
source: "worker",
|
|
153
|
+
status: "running",
|
|
154
|
+
runId: runResult.runId,
|
|
155
|
+
sessionKey,
|
|
156
|
+
message: "Model rate limit reached. OpenClaw is retrying upstream; TeamClaw will keep waiting for the task to continue.",
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const clearRateLimitWaiting = (): void => {
|
|
161
|
+
rateLimitState.active = false;
|
|
162
|
+
rateLimitState.visibleAt = undefined;
|
|
163
|
+
rateLimitState.nextProbeAt = undefined;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const markBackgroundWorkWaiting = async (): Promise<void> => {
|
|
167
|
+
if (backgroundWaitState.active) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
backgroundWaitState.active = true;
|
|
172
|
+
backgroundWaitState.visibleAt = now;
|
|
173
|
+
backgroundWaitState.nextProbeAt = now + BACKGROUND_WORK_PROBE_MS;
|
|
174
|
+
await emitExecutionEvent({
|
|
175
|
+
type: "progress",
|
|
176
|
+
phase: "background_work_waiting",
|
|
177
|
+
source: "worker",
|
|
178
|
+
status: "running",
|
|
179
|
+
runId: runResult.runId,
|
|
180
|
+
sessionKey,
|
|
181
|
+
message: "The worker ended its last turn while background work was still running. TeamClaw will keep checking until the real final deliverable is ready.",
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const clearBackgroundWorkWaiting = (): void => {
|
|
186
|
+
backgroundWaitState.active = false;
|
|
187
|
+
backgroundWaitState.visibleAt = undefined;
|
|
188
|
+
backgroundWaitState.nextProbeAt = undefined;
|
|
189
|
+
};
|
|
98
190
|
|
|
99
191
|
const syncSessionProgress = async (): Promise<void> => {
|
|
100
192
|
const sessionMessages = await runtime.subagent.getSessionMessages({
|
|
@@ -106,8 +198,25 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
106
198
|
const entries = buildSessionProgressEntries(progressSnapshot.latestMessages, taskMessage);
|
|
107
199
|
const newEntries = getNewSessionProgressEntries(entries, progressSnapshot.fingerprints);
|
|
108
200
|
progressSnapshot.fingerprints = entries.map((entry) => entry.fingerprint);
|
|
201
|
+
progressSnapshot.childSessionKeys = mergeChildSessionKeys(
|
|
202
|
+
progressSnapshot.childSessionKeys,
|
|
203
|
+
collectChildSessionKeys(progressSnapshot.latestMessages),
|
|
204
|
+
);
|
|
109
205
|
|
|
110
206
|
for (const entry of newEntries) {
|
|
207
|
+
if (entry.isRateLimit) {
|
|
208
|
+
await markRateLimitWaiting();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (rateLimitState.active && isStillWaitingResponse(entry.message)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (rateLimitState.active && isInternalRetryPrompt(entry.message, entry.stream)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (rateLimitState.active) {
|
|
218
|
+
clearRateLimitWaiting();
|
|
219
|
+
}
|
|
111
220
|
if (entry.stream === "assistant") {
|
|
112
221
|
progressSnapshot.lastAssistantMessage = entry.message;
|
|
113
222
|
}
|
|
@@ -121,6 +230,146 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
121
230
|
message: entry.message,
|
|
122
231
|
});
|
|
123
232
|
}
|
|
233
|
+
|
|
234
|
+
if (Date.now() - progressSnapshot.lastChildPollAt >= CHILD_SESSION_PROGRESS_POLL_INTERVAL_MS) {
|
|
235
|
+
progressSnapshot.lastChildPollAt = Date.now();
|
|
236
|
+
const childRateLimitDetected = await syncChildSessionRateLimits(runtime, progressSnapshot);
|
|
237
|
+
if (childRateLimitDetected) {
|
|
238
|
+
await markRateLimitWaiting();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const extractSessionAssistantTurn = async (): Promise<AssistantTurnSnapshot> => {
|
|
244
|
+
let turn = extractLastAssistantTurn(progressSnapshot.latestMessages);
|
|
245
|
+
if (!turn.text && !turn.backgroundPending) {
|
|
246
|
+
const sessionMessages = await runtime.subagent.getSessionMessages({
|
|
247
|
+
sessionKey,
|
|
248
|
+
limit: 100,
|
|
249
|
+
});
|
|
250
|
+
progressSnapshot.latestMessages = Array.isArray(sessionMessages.messages) ? sessionMessages.messages : [];
|
|
251
|
+
turn = extractLastAssistantTurn(sessionMessages.messages);
|
|
252
|
+
}
|
|
253
|
+
return turn;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const probeRateLimitedTaskCompletion = async (): Promise<string | null> => {
|
|
257
|
+
rateLimitState.probeCount += 1;
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
rateLimitState.visibleAt = now;
|
|
260
|
+
rateLimitState.nextProbeAt = now + RATE_LIMIT_STALL_PROBE_MS;
|
|
261
|
+
await emitExecutionEvent({
|
|
262
|
+
type: "progress",
|
|
263
|
+
phase: "model_rate_limit_probe",
|
|
264
|
+
source: "worker",
|
|
265
|
+
status: "running",
|
|
266
|
+
runId: runResult.runId,
|
|
267
|
+
sessionKey,
|
|
268
|
+
message: `Model rate limit has delayed task progress for over ${formatDuration(RATE_LIMIT_STALL_PROBE_MS)}. Re-checking whether the current task has already completed.`,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const probeRun = await runtime.subagent.run({
|
|
272
|
+
sessionKey,
|
|
273
|
+
message: buildRateLimitProbeMessage(taskId, roleDef?.label ?? role),
|
|
274
|
+
extraSystemPrompt: roleSystemPrompt,
|
|
275
|
+
idempotencyKey: `${idempotencyKey ?? `teamclaw-${taskId}`}:rate-limit-probe:${rateLimitState.probeCount}`,
|
|
276
|
+
});
|
|
277
|
+
const probeWait = await runtime.subagent.waitForRun({
|
|
278
|
+
runId: probeRun.runId,
|
|
279
|
+
timeoutMs: RATE_LIMIT_PROBE_TIMEOUT_MS,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
await syncSessionProgress();
|
|
284
|
+
} catch (err) {
|
|
285
|
+
logger.debug?.(`TeamClaw: failed probe session sync for ${taskId}: ${String(err)}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (probeWait.status !== "ok") {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const probeTurn = await extractSessionAssistantTurn();
|
|
293
|
+
if (!probeTurn.text || probeTurn.backgroundPending || isRateLimitMessage(probeTurn.text) || isStillWaitingResponse(probeTurn.text)) {
|
|
294
|
+
await emitExecutionEvent({
|
|
295
|
+
type: "progress",
|
|
296
|
+
phase: "model_rate_limit_still_waiting",
|
|
297
|
+
source: "worker",
|
|
298
|
+
status: "running",
|
|
299
|
+
runId: runResult.runId,
|
|
300
|
+
sessionKey,
|
|
301
|
+
message: "The task is still waiting on model availability. TeamClaw will continue waiting.",
|
|
302
|
+
});
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
clearRateLimitWaiting();
|
|
307
|
+
return probeTurn.text;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const probeBackgroundTaskCompletion = async (): Promise<AssistantTurnSnapshot | null> => {
|
|
311
|
+
backgroundWaitState.probeCount += 1;
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
backgroundWaitState.visibleAt = now;
|
|
314
|
+
backgroundWaitState.nextProbeAt = now + BACKGROUND_WORK_PROBE_MS;
|
|
315
|
+
await emitExecutionEvent({
|
|
316
|
+
type: "progress",
|
|
317
|
+
phase: "background_work_probe",
|
|
318
|
+
source: "worker",
|
|
319
|
+
status: "running",
|
|
320
|
+
runId: runResult.runId,
|
|
321
|
+
sessionKey,
|
|
322
|
+
message: `Background work has been running for over ${formatDuration(BACKGROUND_WORK_PROBE_MS)}. Re-checking whether the original task is now complete.`,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const probeRun = await runtime.subagent.run({
|
|
326
|
+
sessionKey,
|
|
327
|
+
message: buildBackgroundWorkProbeMessage(taskId, roleDef?.label ?? role),
|
|
328
|
+
extraSystemPrompt: roleSystemPrompt,
|
|
329
|
+
idempotencyKey: `${idempotencyKey ?? `teamclaw-${taskId}`}:background-work-probe:${backgroundWaitState.probeCount}`,
|
|
330
|
+
});
|
|
331
|
+
const probeWait = await runtime.subagent.waitForRun({
|
|
332
|
+
runId: probeRun.runId,
|
|
333
|
+
timeoutMs: Math.min(
|
|
334
|
+
BACKGROUND_WORK_PROBE_TIMEOUT_MS,
|
|
335
|
+
Math.max(1_000, deadline - Date.now()),
|
|
336
|
+
),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
await syncSessionProgress();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
logger.debug?.(`TeamClaw: failed background probe session sync for ${taskId}: ${String(err)}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (probeWait.status !== "ok") {
|
|
346
|
+
if (probeWait.status === "error" && isRateLimitMessage(probeWait.error || "")) {
|
|
347
|
+
await markRateLimitWaiting();
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const probeTurn = await extractSessionAssistantTurn();
|
|
353
|
+
if (
|
|
354
|
+
!probeTurn.text ||
|
|
355
|
+
probeTurn.backgroundPending ||
|
|
356
|
+
isRateLimitMessage(probeTurn.text) ||
|
|
357
|
+
isStillWaitingResponse(probeTurn.text)
|
|
358
|
+
) {
|
|
359
|
+
await emitExecutionEvent({
|
|
360
|
+
type: "progress",
|
|
361
|
+
phase: "background_work_still_waiting",
|
|
362
|
+
source: "worker",
|
|
363
|
+
status: "running",
|
|
364
|
+
runId: runResult.runId,
|
|
365
|
+
sessionKey,
|
|
366
|
+
message: "The task is still waiting on background work. TeamClaw will continue waiting.",
|
|
367
|
+
});
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
clearBackgroundWorkWaiting();
|
|
372
|
+
return probeTurn;
|
|
124
373
|
};
|
|
125
374
|
|
|
126
375
|
let keepPolling = true;
|
|
@@ -140,11 +389,40 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
140
389
|
})();
|
|
141
390
|
|
|
142
391
|
let waitResult;
|
|
392
|
+
let completionOverride: string | null = null;
|
|
143
393
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
394
|
+
while (true) {
|
|
395
|
+
const remainingMs = deadline - Date.now();
|
|
396
|
+
if (remainingMs <= 0) {
|
|
397
|
+
waitResult = { status: "timeout" as const };
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (rateLimitState.active && (rateLimitState.nextProbeAt ?? Number.POSITIVE_INFINITY) <= Date.now()) {
|
|
402
|
+
completionOverride = await probeRateLimitedTaskCompletion();
|
|
403
|
+
if (completionOverride) {
|
|
404
|
+
waitResult = { status: "ok" as const };
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const sliceTimeoutMs = Math.max(1_000, Math.min(RUN_WAIT_SLICE_MS, remainingMs));
|
|
410
|
+
waitResult = await runtime.subagent.waitForRun({
|
|
411
|
+
runId: runResult.runId,
|
|
412
|
+
timeoutMs: sliceTimeoutMs,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (waitResult.status === "ok") {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
if (waitResult.status === "error") {
|
|
419
|
+
if (isRateLimitMessage(waitResult.error || "")) {
|
|
420
|
+
await markRateLimitWaiting();
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
148
426
|
} finally {
|
|
149
427
|
keepPolling = false;
|
|
150
428
|
await pollSessionProgress;
|
|
@@ -157,25 +435,45 @@ export function createRoleTaskExecutor(deps: RoleTaskExecutorDeps) {
|
|
|
157
435
|
}
|
|
158
436
|
|
|
159
437
|
if (waitResult.status === "ok") {
|
|
160
|
-
let
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
438
|
+
let assistantTurn = completionOverride
|
|
439
|
+
? buildAssistantTurnSnapshot(completionOverride)
|
|
440
|
+
: await extractSessionAssistantTurn();
|
|
441
|
+
while (isBackgroundWorkPendingTurn(assistantTurn)) {
|
|
442
|
+
await markBackgroundWorkWaiting();
|
|
443
|
+
const remainingMs = deadline - Date.now();
|
|
444
|
+
if (remainingMs <= 0) {
|
|
445
|
+
waitResult = { status: "timeout" as const };
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
const nextProbeAt = backgroundWaitState.nextProbeAt ?? (Date.now() + BACKGROUND_WORK_PROBE_MS);
|
|
449
|
+
const delayMs = Math.max(1_000, Math.min(nextProbeAt - Date.now(), remainingMs));
|
|
450
|
+
await delay(delayMs);
|
|
451
|
+
const probeTurn = await probeBackgroundTaskCompletion();
|
|
452
|
+
if (probeTurn) {
|
|
453
|
+
assistantTurn = probeTurn;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
assistantTurn = await extractSessionAssistantTurn();
|
|
175
457
|
}
|
|
458
|
+
if (waitResult.status === "ok") {
|
|
459
|
+
if (rateLimitState.active) {
|
|
460
|
+
clearRateLimitWaiting();
|
|
461
|
+
}
|
|
462
|
+
const result = assistantTurn.text;
|
|
463
|
+
if (result && normalizeComparableText(result) !== normalizeComparableText(progressSnapshot.lastAssistantMessage)) {
|
|
464
|
+
await emitExecutionEvent({
|
|
465
|
+
type: "output",
|
|
466
|
+
phase: "final_output",
|
|
467
|
+
source: "subagent",
|
|
468
|
+
message: result,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
176
471
|
|
|
177
|
-
|
|
178
|
-
|
|
472
|
+
clearBackgroundWorkWaiting();
|
|
473
|
+
logger.info(`TeamClaw: task ${taskId} completed successfully as ${role}`);
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
clearBackgroundWorkWaiting();
|
|
179
477
|
}
|
|
180
478
|
|
|
181
479
|
if (waitResult.status === "timeout") {
|
|
@@ -261,12 +559,73 @@ function buildSessionProgressEntries(messages: unknown[], taskMessage: string):
|
|
|
261
559
|
message: rendered.message,
|
|
262
560
|
phase: rendered.stream,
|
|
263
561
|
stream: rendered.stream,
|
|
562
|
+
isRateLimit: isRateLimitMessage(rendered.message),
|
|
264
563
|
});
|
|
265
564
|
}
|
|
266
565
|
|
|
267
566
|
return entries;
|
|
268
567
|
}
|
|
269
568
|
|
|
569
|
+
function collectChildSessionKeys(messages: unknown[]): string[] {
|
|
570
|
+
const keys = new Set<string>();
|
|
571
|
+
for (const message of messages) {
|
|
572
|
+
if (!message || typeof message !== "object") {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const content = (message as { content?: unknown }).content;
|
|
576
|
+
const text = typeof content === "string"
|
|
577
|
+
? content
|
|
578
|
+
: Array.isArray(content)
|
|
579
|
+
? content
|
|
580
|
+
.map((entry) => (entry && typeof entry === "object" && typeof (entry as { text?: unknown }).text === "string")
|
|
581
|
+
? (entry as { text: string }).text
|
|
582
|
+
: "")
|
|
583
|
+
.filter(Boolean)
|
|
584
|
+
.join("\n")
|
|
585
|
+
: "";
|
|
586
|
+
for (const match of text.matchAll(/"childSessionKey"\s*:\s*"([^"]+)"/g)) {
|
|
587
|
+
const childSessionKey = match[1]?.trim();
|
|
588
|
+
if (childSessionKey) {
|
|
589
|
+
keys.add(childSessionKey);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return Array.from(keys);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function mergeChildSessionKeys(existing: string[], discovered: string[]): string[] {
|
|
597
|
+
const keys = new Set(existing);
|
|
598
|
+
for (const childSessionKey of discovered) {
|
|
599
|
+
keys.add(childSessionKey);
|
|
600
|
+
}
|
|
601
|
+
return Array.from(keys);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function syncChildSessionRateLimits(
|
|
605
|
+
runtime: OpenClawPluginApi["runtime"],
|
|
606
|
+
snapshot: SessionProgressSnapshot,
|
|
607
|
+
): Promise<boolean> {
|
|
608
|
+
let detected = false;
|
|
609
|
+
for (const childSessionKey of snapshot.childSessionKeys) {
|
|
610
|
+
try {
|
|
611
|
+
const sessionMessages = await runtime.subagent.getSessionMessages({
|
|
612
|
+
sessionKey: childSessionKey,
|
|
613
|
+
limit: SESSION_PROGRESS_MESSAGE_LIMIT,
|
|
614
|
+
});
|
|
615
|
+
const entries = buildSessionProgressEntries(sessionMessages.messages, "");
|
|
616
|
+
const previousFingerprints = snapshot.childFingerprints.get(childSessionKey) ?? [];
|
|
617
|
+
const newEntries = getNewSessionProgressEntries(entries, previousFingerprints);
|
|
618
|
+
snapshot.childFingerprints.set(childSessionKey, entries.map((entry) => entry.fingerprint));
|
|
619
|
+
if (newEntries.some((entry) => entry.isRateLimit)) {
|
|
620
|
+
detected = true;
|
|
621
|
+
}
|
|
622
|
+
} catch (_err) {
|
|
623
|
+
// Child session updates are best-effort only.
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return detected;
|
|
627
|
+
}
|
|
628
|
+
|
|
270
629
|
function getNewSessionProgressEntries(
|
|
271
630
|
entries: SessionProgressEntry[],
|
|
272
631
|
previousFingerprints: string[],
|
|
@@ -442,11 +801,51 @@ function buildTaskMessage(taskDescription: string, taskId: string, roleLabel: st
|
|
|
442
801
|
"- Do NOT mark the task completed or failed via progress tools. Return the final deliverable (or raise an error) and let TeamClaw close the task.",
|
|
443
802
|
"- If critical information is missing and you cannot proceed safely, request clarification and wait instead of guessing.",
|
|
444
803
|
"- If more work is needed, mention it briefly in your result or use a handoff/review tool on this same task.",
|
|
804
|
+
"- Before your final reply, submit a structured worker result contract with teamclaw_submit_result_contract so TeamClaw can route the next step without parsing prose.",
|
|
805
|
+
`- Do NOT use sessions_yield or end your turn while background work, coding agents, or process sessions are still running; if the task is not complete yet, reply with exactly ${RATE_LIMIT_WAITING_SENTINEL}.`,
|
|
806
|
+
"- Never return 'running in background' as the final result for a TeamClaw task. If you spawn a helper session, keep monitoring it and only return after you have the actual deliverable.",
|
|
807
|
+
"- Use structured fields on progress, review, handoff, and messaging tools whenever coordination is needed.",
|
|
445
808
|
`- When naming a role, use exact TeamClaw role IDs: ${TEAMCLAW_ROLE_IDS_TEXT}.`,
|
|
446
809
|
].join("\n");
|
|
447
810
|
}
|
|
448
811
|
|
|
449
|
-
function
|
|
812
|
+
function buildRateLimitProbeMessage(taskId: string, roleLabel: string): string {
|
|
813
|
+
return [
|
|
814
|
+
`This is a follow-up check for task ${taskId} (${roleLabel}).`,
|
|
815
|
+
"The earlier run appears to be delayed by upstream model rate limiting.",
|
|
816
|
+
"Do not restart the task from scratch.",
|
|
817
|
+
"If the original task is fully complete now, immediately submit the structured result contract and provide the final result for that original task.",
|
|
818
|
+
`If the original task is not complete yet, reply with exactly ${RATE_LIMIT_WAITING_SENTINEL}.`,
|
|
819
|
+
].join("\n");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function buildBackgroundWorkProbeMessage(taskId: string, roleLabel: string): string {
|
|
823
|
+
return [
|
|
824
|
+
`This is a follow-up check for task ${taskId} (${roleLabel}).`,
|
|
825
|
+
"Your previous turn ended while background work was still running.",
|
|
826
|
+
"Do not restart the task from scratch.",
|
|
827
|
+
"Inspect the background coding or process session you previously started, continue from the existing workspace/session state, and only finalize once the original task deliverable is genuinely complete.",
|
|
828
|
+
"Do not call sessions_yield again unless you are still explicitly waiting on unfinished background work.",
|
|
829
|
+
"If the original task is fully complete now, immediately submit the structured result contract and provide the final result for that original task.",
|
|
830
|
+
`If the original task is not complete yet, reply with exactly ${RATE_LIMIT_WAITING_SENTINEL}.`,
|
|
831
|
+
].join("\n");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function buildAssistantTurnSnapshot(text: string, toolCalls: string[] = []): AssistantTurnSnapshot {
|
|
835
|
+
const normalizedText = String(text || "").trim();
|
|
836
|
+
const normalizedToolCalls = toolCalls
|
|
837
|
+
.map((name) => String(name || "").trim().toLowerCase())
|
|
838
|
+
.filter(Boolean);
|
|
839
|
+
const yielded = normalizedToolCalls.includes("sessions_yield");
|
|
840
|
+
return {
|
|
841
|
+
text: normalizedText,
|
|
842
|
+
toolCalls: normalizedToolCalls,
|
|
843
|
+
yielded,
|
|
844
|
+
backgroundPending: yielded || isBackgroundWorkPendingMessage(normalizedText),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function extractLastAssistantTurn(messages: unknown[]): AssistantTurnSnapshot {
|
|
450
849
|
const assistantMessages = messages.filter((message): message is { role?: unknown; content?: unknown } => {
|
|
451
850
|
if (!message || typeof message !== "object") {
|
|
452
851
|
return false;
|
|
@@ -456,11 +855,11 @@ function extractLastAssistantText(messages: unknown[]): string {
|
|
|
456
855
|
|
|
457
856
|
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
458
857
|
if (!lastAssistant) {
|
|
459
|
-
return "";
|
|
858
|
+
return buildAssistantTurnSnapshot("");
|
|
460
859
|
}
|
|
461
860
|
|
|
462
861
|
if (typeof lastAssistant.content === "string") {
|
|
463
|
-
return lastAssistant.content;
|
|
862
|
+
return buildAssistantTurnSnapshot(lastAssistant.content);
|
|
464
863
|
}
|
|
465
864
|
|
|
466
865
|
if (Array.isArray(lastAssistant.content)) {
|
|
@@ -470,10 +869,60 @@ function extractLastAssistantText(messages: unknown[]): string {
|
|
|
470
869
|
})
|
|
471
870
|
.map((block) => (typeof block.text === "string" ? block.text : ""))
|
|
472
871
|
.filter(Boolean);
|
|
473
|
-
|
|
474
|
-
|
|
872
|
+
const toolCalls = lastAssistant.content
|
|
873
|
+
.filter((block): block is { type?: unknown; name?: unknown } => {
|
|
874
|
+
return !!block
|
|
875
|
+
&& typeof block === "object"
|
|
876
|
+
&& TOOL_CALL_BLOCK_TYPES.has(normalizeBlockType((block as { type?: unknown }).type));
|
|
877
|
+
})
|
|
878
|
+
.map((block) => (typeof block.name === "string" ? block.name : ""))
|
|
879
|
+
.filter(Boolean);
|
|
880
|
+
if (textBlocks.length > 0 || toolCalls.length > 0) {
|
|
881
|
+
return buildAssistantTurnSnapshot(textBlocks.join("\n"), toolCalls);
|
|
475
882
|
}
|
|
476
883
|
}
|
|
477
884
|
|
|
478
|
-
return JSON.stringify(lastAssistant);
|
|
885
|
+
return buildAssistantTurnSnapshot(JSON.stringify(lastAssistant));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function isRateLimitMessage(value: string): boolean {
|
|
889
|
+
return /(rate[_ ]limit|too many requests|429\b|resource has been exhausted|tokens per day|quota|throttl)/i.test(
|
|
890
|
+
String(value || ""),
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function isStillWaitingResponse(value: string): boolean {
|
|
895
|
+
const normalized = String(value || "").trim();
|
|
896
|
+
if (!normalized) {
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
if (normalized === RATE_LIMIT_WAITING_SENTINEL) {
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
return /(still waiting|continue waiting|not complete yet|尚未完成|继续等待|仍在等待)/i.test(normalized);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function isInternalRetryPrompt(value: string, stream?: string): boolean {
|
|
906
|
+
if (stream !== "user") {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
const normalized = String(value || "").trim();
|
|
910
|
+
if (!normalized) {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
return /continue where you left off\. the previous model attempt failed or timed out\./i.test(normalized);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function isBackgroundWorkPendingMessage(value: string): boolean {
|
|
917
|
+
const normalized = String(value || "").trim();
|
|
918
|
+
if (!normalized) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
return /(running in background|background session|command still running \(session|monitor progress and report back when complete|后台.*运行中|后台.*会在完成后汇报|后台.*完成后再汇报)/i.test(
|
|
922
|
+
normalized,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function isBackgroundWorkPendingTurn(turn: AssistantTurnSnapshot): boolean {
|
|
927
|
+
return turn.backgroundPending || isStillWaitingResponse(turn.text);
|
|
479
928
|
}
|