openclaw-clawtown-plugin 1.1.17 → 1.1.19
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/reporter.ts +377 -30
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-clawtown-plugin",
|
|
3
3
|
"name": "OpenClaw Clawtown Plugin",
|
|
4
4
|
"description": "Connects an OpenClaw agent to OpenClaw Forum and reports forum actions",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.19",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
package/reporter.ts
CHANGED
|
@@ -162,6 +162,37 @@ interface PairingStatusResult {
|
|
|
162
162
|
displayName: string;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
interface AgentOutputInspection {
|
|
166
|
+
text: string;
|
|
167
|
+
jsonCandidateCount: number;
|
|
168
|
+
envelopeDetected: boolean;
|
|
169
|
+
envelopePayloadCount: number;
|
|
170
|
+
envelopePayloadTextCount: number;
|
|
171
|
+
envelopePayloadTextLength: number;
|
|
172
|
+
envelopePayloadPreview: string;
|
|
173
|
+
extractedFromEnvelope: boolean;
|
|
174
|
+
extractedJsonObject: boolean;
|
|
175
|
+
lineCount: number;
|
|
176
|
+
lastNonEmptyLinePreview: string;
|
|
177
|
+
rawOutputPreview: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface AgentTurnDiagnostics extends AgentOutputInspection {
|
|
181
|
+
sessionId: string;
|
|
182
|
+
transport: "powershell" | "openclaw";
|
|
183
|
+
durationMs: number;
|
|
184
|
+
stdoutLength: number;
|
|
185
|
+
stderrLength: number;
|
|
186
|
+
stderrPreview: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface AgentTurnResult {
|
|
190
|
+
stdout: string;
|
|
191
|
+
stderr: string;
|
|
192
|
+
extractedText: string;
|
|
193
|
+
diagnostics: AgentTurnDiagnostics;
|
|
194
|
+
}
|
|
195
|
+
|
|
165
196
|
class Reporter {
|
|
166
197
|
private userId: string;
|
|
167
198
|
private apiKey: string;
|
|
@@ -187,6 +218,8 @@ class Reporter {
|
|
|
187
218
|
private taskQueue: ServerPushMessage[] = [];
|
|
188
219
|
private processingTask = false;
|
|
189
220
|
private paused = false;
|
|
221
|
+
private activeTaskDedupKey = "";
|
|
222
|
+
private queuedTaskDedupKeys = new Set<string>();
|
|
190
223
|
private pendingContextUpdates = new Map<string, PendingQuestionContextUpdate>();
|
|
191
224
|
private sessionHintLogged = false;
|
|
192
225
|
private instanceLockPath: string | null = null;
|
|
@@ -572,8 +605,14 @@ class Reporter {
|
|
|
572
605
|
if (message.event === "task_push") {
|
|
573
606
|
const qid = String((message.payload as any)?.questionId ?? "").trim();
|
|
574
607
|
const tid = String((message.payload as any)?.taskId ?? "").trim();
|
|
608
|
+
const dedupKey = buildTaskDedupKey(message);
|
|
575
609
|
this.lastTaskPushAt = Date.now();
|
|
610
|
+
if (dedupKey && (this.activeTaskDedupKey === dedupKey || this.queuedTaskDedupKeys.has(dedupKey))) {
|
|
611
|
+
console.log(`[forum-reporter-v2] duplicate task_push dropped: ${String(message.taskType ?? "unknown")}${qid ? ` questionId=${qid}` : ""}${tid ? ` taskId=${tid}` : ""}`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
576
614
|
console.log(`[forum-reporter-v2] task_push received: ${String(message.taskType ?? "unknown")}${qid ? ` questionId=${qid}` : ""}${tid ? ` taskId=${tid}` : ""}`);
|
|
615
|
+
if (dedupKey) this.queuedTaskDedupKeys.add(dedupKey);
|
|
577
616
|
this.taskQueue.push(message);
|
|
578
617
|
void this.processTaskQueue();
|
|
579
618
|
}
|
|
@@ -586,11 +625,23 @@ class Reporter {
|
|
|
586
625
|
while (this.taskQueue.length > 0) {
|
|
587
626
|
const task = this.taskQueue.shift();
|
|
588
627
|
if (!task) break;
|
|
628
|
+
const dedupKey = buildTaskDedupKey(task);
|
|
629
|
+
if (dedupKey) {
|
|
630
|
+
this.queuedTaskDedupKeys.delete(dedupKey);
|
|
631
|
+
this.activeTaskDedupKey = dedupKey;
|
|
632
|
+
}
|
|
589
633
|
if (this.paused) {
|
|
590
634
|
console.log("[forum-reporter-v2] paused; dropping queued task");
|
|
635
|
+
this.activeTaskDedupKey = "";
|
|
591
636
|
continue;
|
|
592
637
|
}
|
|
593
|
-
|
|
638
|
+
try {
|
|
639
|
+
await this.executeTask(task);
|
|
640
|
+
} finally {
|
|
641
|
+
if (this.activeTaskDedupKey === dedupKey) {
|
|
642
|
+
this.activeTaskDedupKey = "";
|
|
643
|
+
}
|
|
644
|
+
}
|
|
594
645
|
}
|
|
595
646
|
} finally {
|
|
596
647
|
this.processingTask = false;
|
|
@@ -620,6 +671,19 @@ class Reporter {
|
|
|
620
671
|
let instructions = baseInstructions;
|
|
621
672
|
let rawOutput = "";
|
|
622
673
|
let executeErrorMessage = "";
|
|
674
|
+
let lastTurnDiagnostics: AgentTurnDiagnostics | null = null;
|
|
675
|
+
const runTaskTurn = async (message: string, timeoutSeconds: number, timeoutMs: number, taskKey: string) => {
|
|
676
|
+
const turn = await this.runVisibleAgentTurn(message, timeoutSeconds, timeoutMs, { taskKey });
|
|
677
|
+
lastTurnDiagnostics = turn.diagnostics;
|
|
678
|
+
return turn.extractedText;
|
|
679
|
+
};
|
|
680
|
+
const logTaskFailureDiagnostics = (stage: string, reason: TaskFailureReason, outputText: string) => {
|
|
681
|
+
if (!lastTurnDiagnostics) return;
|
|
682
|
+
if (reason.code !== "empty_model_output" && reason.code !== "invalid_json_output") return;
|
|
683
|
+
console.warn(
|
|
684
|
+
`[forum-reporter-v2] model turn diagnostics (${taskType}/${stage}): ${formatAgentTurnDiagnostics(lastTurnDiagnostics, reason, outputText)}`,
|
|
685
|
+
);
|
|
686
|
+
};
|
|
623
687
|
const reportClientFailure = async (reason: TaskFailureReason, outputText: string) => {
|
|
624
688
|
const kind = actionKindForTaskType(taskType);
|
|
625
689
|
const failureMeta = buildClientFailureMeta(taskType, outputText);
|
|
@@ -638,14 +702,31 @@ class Reporter {
|
|
|
638
702
|
modelOutputPreview: failureMeta.modelOutputPreview,
|
|
639
703
|
candidateAnswerLength: failureMeta.candidateAnswerLength,
|
|
640
704
|
candidateAnswerPreview: failureMeta.candidateAnswerPreview,
|
|
705
|
+
agentTurnSessionId: lastTurnDiagnostics?.sessionId ?? "",
|
|
706
|
+
agentTurnTransport: lastTurnDiagnostics?.transport ?? "",
|
|
707
|
+
agentTurnDurationMs: lastTurnDiagnostics?.durationMs ?? 0,
|
|
708
|
+
agentTurnStdoutLength: lastTurnDiagnostics?.stdoutLength ?? 0,
|
|
709
|
+
agentTurnStderrLength: lastTurnDiagnostics?.stderrLength ?? 0,
|
|
710
|
+
agentTurnJsonCandidateCount: lastTurnDiagnostics?.jsonCandidateCount ?? 0,
|
|
711
|
+
agentTurnEnvelopeDetected: Boolean(lastTurnDiagnostics?.envelopeDetected),
|
|
712
|
+
agentTurnEnvelopePayloadCount: lastTurnDiagnostics?.envelopePayloadCount ?? 0,
|
|
713
|
+
agentTurnEnvelopePayloadTextCount: lastTurnDiagnostics?.envelopePayloadTextCount ?? 0,
|
|
714
|
+
agentTurnEnvelopePayloadTextLength: lastTurnDiagnostics?.envelopePayloadTextLength ?? 0,
|
|
715
|
+
agentTurnOutputPreview: lastTurnDiagnostics?.rawOutputPreview ?? "",
|
|
716
|
+
agentTurnPayloadPreview: lastTurnDiagnostics?.envelopePayloadPreview ?? "",
|
|
717
|
+
agentTurnLastLinePreview: lastTurnDiagnostics?.lastNonEmptyLinePreview ?? "",
|
|
718
|
+
agentTurnStderrPreview: lastTurnDiagnostics?.stderrPreview ?? "",
|
|
641
719
|
},
|
|
642
720
|
rawOutput: outputText,
|
|
643
721
|
});
|
|
644
722
|
};
|
|
645
723
|
try {
|
|
646
|
-
rawOutput =
|
|
647
|
-
|
|
648
|
-
|
|
724
|
+
rawOutput = await runTaskTurn(
|
|
725
|
+
instructions,
|
|
726
|
+
TASK_FIRST_TURN_TIMEOUT_SECONDS,
|
|
727
|
+
ACTION_MODEL_TIMEOUT_MS,
|
|
728
|
+
tid || qid || taskType,
|
|
729
|
+
);
|
|
649
730
|
} catch (error: any) {
|
|
650
731
|
executeErrorMessage = String(error?.message ?? error ?? "unknown");
|
|
651
732
|
console.warn(`[forum-reporter-v2] execute task failed: ${executeErrorMessage}`);
|
|
@@ -654,6 +735,7 @@ class Reporter {
|
|
|
654
735
|
let normalized = normalizeTaskResult(taskType, rawOutput, payload);
|
|
655
736
|
if (!normalized) {
|
|
656
737
|
const firstFailureReason = diagnoseTaskResultFailure(taskType, rawOutput, payload, executeErrorMessage);
|
|
738
|
+
logTaskFailureDiagnostics("first-attempt", firstFailureReason, rawOutput);
|
|
657
739
|
if (firstFailureReason.code === "model_turn_failed") {
|
|
658
740
|
const timeoutFeedback = humanizeFailureReason(firstFailureReason, taskType);
|
|
659
741
|
console.warn(`[forum-reporter-v2] first attempt timeout for taskType=${taskType}, retry once with longer timeout`);
|
|
@@ -661,9 +743,12 @@ class Reporter {
|
|
|
661
743
|
rawOutput = "";
|
|
662
744
|
executeErrorMessage = "";
|
|
663
745
|
try {
|
|
664
|
-
rawOutput =
|
|
665
|
-
|
|
666
|
-
|
|
746
|
+
rawOutput = await runTaskTurn(
|
|
747
|
+
instructions,
|
|
748
|
+
TASK_TIMEOUT_RETRY_SECONDS,
|
|
749
|
+
ACTION_MODEL_TIMEOUT_MS,
|
|
750
|
+
`${tid || qid || taskType}-timeout-retry1`,
|
|
751
|
+
);
|
|
667
752
|
} catch (error: any) {
|
|
668
753
|
executeErrorMessage = String(error?.message ?? error ?? "unknown");
|
|
669
754
|
console.warn(`[forum-reporter-v2] timeout-retry execute failed: ${executeErrorMessage}`);
|
|
@@ -671,6 +756,7 @@ class Reporter {
|
|
|
671
756
|
normalized = normalizeTaskResult(taskType, rawOutput, payload);
|
|
672
757
|
if (!normalized) {
|
|
673
758
|
const timeoutRetryFailure = diagnoseTaskResultFailure(taskType, rawOutput, payload, executeErrorMessage);
|
|
759
|
+
logTaskFailureDiagnostics("timeout-retry", timeoutRetryFailure, rawOutput);
|
|
674
760
|
await reportClientFailure(timeoutRetryFailure, rawOutput);
|
|
675
761
|
return;
|
|
676
762
|
}
|
|
@@ -682,9 +768,12 @@ class Reporter {
|
|
|
682
768
|
rawOutput = "";
|
|
683
769
|
executeErrorMessage = "";
|
|
684
770
|
try {
|
|
685
|
-
rawOutput =
|
|
686
|
-
|
|
687
|
-
|
|
771
|
+
rawOutput = await runTaskTurn(
|
|
772
|
+
instructions,
|
|
773
|
+
TASK_FIRST_TURN_TIMEOUT_SECONDS,
|
|
774
|
+
ACTION_MODEL_TIMEOUT_MS,
|
|
775
|
+
`${tid || qid || taskType}-retry1`,
|
|
776
|
+
);
|
|
688
777
|
} catch (error: any) {
|
|
689
778
|
executeErrorMessage = String(error?.message ?? error ?? "unknown");
|
|
690
779
|
console.warn(`[forum-reporter-v2] retry execute failed: ${executeErrorMessage}`);
|
|
@@ -692,6 +781,7 @@ class Reporter {
|
|
|
692
781
|
normalized = normalizeTaskResult(taskType, rawOutput, payload);
|
|
693
782
|
if (!normalized) {
|
|
694
783
|
const failureReason = diagnoseTaskResultFailure(taskType, rawOutput, payload, executeErrorMessage);
|
|
784
|
+
logTaskFailureDiagnostics("retry1", failureReason, rawOutput);
|
|
695
785
|
await reportClientFailure(failureReason, rawOutput);
|
|
696
786
|
return;
|
|
697
787
|
}
|
|
@@ -712,9 +802,12 @@ class Reporter {
|
|
|
712
802
|
const retryInstructions = buildRetryInstructions(baseInstructions, serverFeedback);
|
|
713
803
|
let retryOutput = "";
|
|
714
804
|
try {
|
|
715
|
-
retryOutput =
|
|
716
|
-
|
|
717
|
-
|
|
805
|
+
retryOutput = await runTaskTurn(
|
|
806
|
+
retryInstructions,
|
|
807
|
+
TASK_FIRST_TURN_TIMEOUT_SECONDS,
|
|
808
|
+
ACTION_MODEL_TIMEOUT_MS,
|
|
809
|
+
`${tid || qid || taskType}-submit-retry1`,
|
|
810
|
+
);
|
|
718
811
|
} catch (error: any) {
|
|
719
812
|
const msg = String(error?.message ?? error ?? "unknown");
|
|
720
813
|
console.warn(`[forum-reporter-v2] submit-retry execute failed: ${msg}`);
|
|
@@ -723,6 +816,7 @@ class Reporter {
|
|
|
723
816
|
const retryNormalized = normalizeTaskResult(taskType, retryOutput, payload);
|
|
724
817
|
if (!retryNormalized) {
|
|
725
818
|
const retryFailure = diagnoseTaskResultFailure(taskType, retryOutput, payload, "");
|
|
819
|
+
logTaskFailureDiagnostics("submit-retry", retryFailure, retryOutput);
|
|
726
820
|
console.warn(`[forum-reporter-v2] submit-retry produced invalid result: ${retryFailure.code}`);
|
|
727
821
|
return;
|
|
728
822
|
}
|
|
@@ -767,11 +861,17 @@ class Reporter {
|
|
|
767
861
|
|
|
768
862
|
if (!submitRes.ok) {
|
|
769
863
|
const reasonCode = String(input.payload?.reasonCode ?? "");
|
|
770
|
-
|
|
864
|
+
const bodyError = String(body.error ?? "").trim();
|
|
865
|
+
const logLine = `[forum-reporter-v2] action-response failed: ${submitRes.status} ${rawText}${reasonCode ? ` (reasonCode=${reasonCode})` : ""}`;
|
|
866
|
+
if (isExpectedSubmitConflict(bodyError)) {
|
|
867
|
+
console.log(logLine);
|
|
868
|
+
} else {
|
|
869
|
+
console.warn(logLine);
|
|
870
|
+
}
|
|
771
871
|
return {
|
|
772
872
|
ok: false,
|
|
773
873
|
status: submitRes.status,
|
|
774
|
-
error:
|
|
874
|
+
error: bodyError || undefined,
|
|
775
875
|
message: String(body.message ?? "").trim() || undefined,
|
|
776
876
|
reasonCode: String(body.reasonCode ?? "").trim() || undefined,
|
|
777
877
|
reasonDetail: String(body.reasonDetail ?? "").trim() || undefined,
|
|
@@ -964,10 +1064,19 @@ class Reporter {
|
|
|
964
1064
|
const prompt = buildV2ActionPrompt(context);
|
|
965
1065
|
let text = "";
|
|
966
1066
|
try {
|
|
967
|
-
const
|
|
1067
|
+
const turn = await this.runVisibleAgentTurn(prompt, 115, ACTION_CONTEXT_TIMEOUT_MS, {
|
|
968
1068
|
taskKey: `context-${context.userId}`,
|
|
969
1069
|
});
|
|
970
|
-
text =
|
|
1070
|
+
text = turn.extractedText;
|
|
1071
|
+
if (!text) {
|
|
1072
|
+
console.warn(
|
|
1073
|
+
`[forum-reporter-v2] action-context model turn diagnostics: ${formatAgentTurnDiagnostics(
|
|
1074
|
+
turn.diagnostics,
|
|
1075
|
+
{ code: "empty_model_output", detail: "model returned empty content" },
|
|
1076
|
+
text,
|
|
1077
|
+
)}`,
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
971
1080
|
} catch (error: any) {
|
|
972
1081
|
const modelTurnError = String(error?.message ?? error ?? "unknown");
|
|
973
1082
|
console.warn(`[forum-reporter-v2] model turn failed, fallback to deterministic action: ${modelTurnError}`);
|
|
@@ -1013,7 +1122,7 @@ class Reporter {
|
|
|
1013
1122
|
timeoutSeconds: number,
|
|
1014
1123
|
timeoutMs: number,
|
|
1015
1124
|
options?: { taskKey?: string },
|
|
1016
|
-
): Promise<
|
|
1125
|
+
): Promise<AgentTurnResult> {
|
|
1017
1126
|
// 每次任务使用一次性 session,避免历史累积污染后续任务。
|
|
1018
1127
|
const baseSessionId = buildOneShotSessionId(this.userId, options?.taskKey);
|
|
1019
1128
|
try {
|
|
@@ -1040,8 +1149,9 @@ class Reporter {
|
|
|
1040
1149
|
message: string,
|
|
1041
1150
|
timeoutSeconds: number,
|
|
1042
1151
|
timeoutMs: number,
|
|
1043
|
-
): Promise<
|
|
1152
|
+
): Promise<AgentTurnResult> {
|
|
1044
1153
|
const isWindows = process.platform === "win32";
|
|
1154
|
+
const startedAt = Date.now();
|
|
1045
1155
|
if (isWindows) {
|
|
1046
1156
|
const script = buildWindowsAgentCommandScript({
|
|
1047
1157
|
agentId: this.openclawAgentId,
|
|
@@ -1070,7 +1180,13 @@ class Reporter {
|
|
|
1070
1180
|
}),
|
|
1071
1181
|
timeoutMs,
|
|
1072
1182
|
);
|
|
1073
|
-
return
|
|
1183
|
+
return buildAgentTurnResult({
|
|
1184
|
+
stdout: String(result.stdout ?? ""),
|
|
1185
|
+
stderr: String(result.stderr ?? ""),
|
|
1186
|
+
sessionId,
|
|
1187
|
+
transport: "powershell",
|
|
1188
|
+
durationMs: Date.now() - startedAt,
|
|
1189
|
+
});
|
|
1074
1190
|
}
|
|
1075
1191
|
|
|
1076
1192
|
const args = [
|
|
@@ -1096,7 +1212,13 @@ class Reporter {
|
|
|
1096
1212
|
}),
|
|
1097
1213
|
timeoutMs,
|
|
1098
1214
|
);
|
|
1099
|
-
return
|
|
1215
|
+
return buildAgentTurnResult({
|
|
1216
|
+
stdout: String(result.stdout ?? ""),
|
|
1217
|
+
stderr: String(result.stderr ?? ""),
|
|
1218
|
+
sessionId,
|
|
1219
|
+
transport: "openclaw",
|
|
1220
|
+
durationMs: Date.now() - startedAt,
|
|
1221
|
+
});
|
|
1100
1222
|
}
|
|
1101
1223
|
|
|
1102
1224
|
private resolveInstanceLockPath(userId: string) {
|
|
@@ -1553,6 +1675,27 @@ function humanizeSubmitFailure(result: SubmitActionResult) {
|
|
|
1553
1675
|
return `提交未通过:${message || error || "unknown_error"}。请修正后重新提交。`;
|
|
1554
1676
|
}
|
|
1555
1677
|
|
|
1678
|
+
function isExpectedSubmitConflict(errorCode: string) {
|
|
1679
|
+
const error = String(errorCode ?? "").trim();
|
|
1680
|
+
return error === "already_answered"
|
|
1681
|
+
|| error === "already_voted"
|
|
1682
|
+
|| error === "answer_slots_full"
|
|
1683
|
+
|| error === "question_not_answering"
|
|
1684
|
+
|| error === "question_not_voting"
|
|
1685
|
+
|| error === "question_not_found"
|
|
1686
|
+
|| error === "answer_not_found"
|
|
1687
|
+
|| error === "task_not_assigned_to_robot"
|
|
1688
|
+
|| error === "task_status_conflict"
|
|
1689
|
+
|| error === "task_not_pending"
|
|
1690
|
+
|| error === "task_not_found"
|
|
1691
|
+
|| error === "task_blocked_for_failed_robot"
|
|
1692
|
+
|| error === "robot_already_has_active_mine_task"
|
|
1693
|
+
|| error === "robot_cannot_mine"
|
|
1694
|
+
|| error === "review_temporarily_unavailable"
|
|
1695
|
+
|| error === "action_not_allowed_in_current_context"
|
|
1696
|
+
|| error === "agent_forum_paused";
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1556
1699
|
function buildRetryInstructions(baseInstructions: string, feedback: string) {
|
|
1557
1700
|
const tip = String(feedback || "").trim();
|
|
1558
1701
|
if (!tip) return baseInstructions;
|
|
@@ -1750,6 +1893,19 @@ function actionKindForTaskType(taskType: PushTaskType): AgentActionKind {
|
|
|
1750
1893
|
return "mine_task";
|
|
1751
1894
|
}
|
|
1752
1895
|
|
|
1896
|
+
function buildTaskDedupKey(task: Pick<ServerPushMessage, "taskType" | "payload"> | null | undefined) {
|
|
1897
|
+
const taskType = String(task?.taskType ?? "").trim();
|
|
1898
|
+
const payload = (task?.payload && typeof task.payload === "object")
|
|
1899
|
+
? task.payload as Record<string, unknown>
|
|
1900
|
+
: {};
|
|
1901
|
+
const questionId = String(payload.questionId ?? "").trim();
|
|
1902
|
+
const taskId = String(payload.taskId ?? "").trim();
|
|
1903
|
+
if (!taskType) return "";
|
|
1904
|
+
if (questionId) return `${taskType}:${questionId}`;
|
|
1905
|
+
if (taskId) return `${taskType}:${taskId}`;
|
|
1906
|
+
return "";
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1753
1909
|
function buildWindowsAgentCommandScript(input: { agentId: string; sessionId: string; message: string; timeoutSeconds: number }) {
|
|
1754
1910
|
const safeMessage = String(input.message ?? "").replace(/\r\n/g, "\n");
|
|
1755
1911
|
const nonMainAgent = String(input.agentId ?? "").trim() && String(input.agentId).trim() !== "main";
|
|
@@ -1772,25 +1928,210 @@ function quoteForPowerShell(value: string) {
|
|
|
1772
1928
|
}
|
|
1773
1929
|
|
|
1774
1930
|
function extractReplyText(output: string): string {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1931
|
+
return inspectAgentOutput(output).text;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function inspectAgentOutput(output: string): AgentOutputInspection {
|
|
1935
|
+
const rawOutput = String(output ?? "");
|
|
1936
|
+
const text = rawOutput.trim();
|
|
1937
|
+
const rawOutputPreview = compactPreview(rawOutput, 220);
|
|
1938
|
+
const jsonCandidates = text ? parseJsonObjectCandidates(text) : [];
|
|
1939
|
+
let envelopeDetected = false;
|
|
1940
|
+
let envelopePayloadCount = 0;
|
|
1941
|
+
let envelopePayloadTextCount = 0;
|
|
1942
|
+
let envelopePayloadTextLength = 0;
|
|
1943
|
+
let envelopePayloadPreview = "";
|
|
1944
|
+
let extractedFromEnvelope = false;
|
|
1945
|
+
let extractedJsonObject = false;
|
|
1946
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1947
|
+
const lastNonEmptyLinePreview = lines.length ? compactPreview(lines[lines.length - 1], 160) : "";
|
|
1948
|
+
|
|
1949
|
+
if (!text) {
|
|
1950
|
+
return {
|
|
1951
|
+
text: "",
|
|
1952
|
+
jsonCandidateCount: 0,
|
|
1953
|
+
envelopeDetected: false,
|
|
1954
|
+
envelopePayloadCount: 0,
|
|
1955
|
+
envelopePayloadTextCount: 0,
|
|
1956
|
+
envelopePayloadTextLength: 0,
|
|
1957
|
+
envelopePayloadPreview: "",
|
|
1958
|
+
extractedFromEnvelope: false,
|
|
1959
|
+
extractedJsonObject: false,
|
|
1960
|
+
lineCount: 0,
|
|
1961
|
+
lastNonEmptyLinePreview: "",
|
|
1962
|
+
rawOutputPreview,
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1778
1966
|
for (let i = jsonCandidates.length - 1; i >= 0; i -= 1) {
|
|
1779
1967
|
try {
|
|
1780
1968
|
const parsed = JSON.parse(jsonCandidates[i]);
|
|
1969
|
+
const envelopeMeta = inspectAgentEnvelope(parsed);
|
|
1970
|
+
if (envelopeMeta.detected) {
|
|
1971
|
+
envelopeDetected = true;
|
|
1972
|
+
envelopePayloadCount = envelopeMeta.payloadCount;
|
|
1973
|
+
envelopePayloadTextCount = envelopeMeta.payloadTextCount;
|
|
1974
|
+
envelopePayloadTextLength = envelopeMeta.payloadTextLength;
|
|
1975
|
+
envelopePayloadPreview = envelopeMeta.payloadPreview;
|
|
1976
|
+
}
|
|
1781
1977
|
const payloadText = extractPayloadTextFromAgentEnvelope(parsed);
|
|
1782
|
-
if (payloadText)
|
|
1783
|
-
|
|
1978
|
+
if (payloadText) {
|
|
1979
|
+
extractedFromEnvelope = true;
|
|
1980
|
+
return {
|
|
1981
|
+
text: payloadText,
|
|
1982
|
+
jsonCandidateCount: jsonCandidates.length,
|
|
1983
|
+
envelopeDetected,
|
|
1984
|
+
envelopePayloadCount,
|
|
1985
|
+
envelopePayloadTextCount,
|
|
1986
|
+
envelopePayloadTextLength,
|
|
1987
|
+
envelopePayloadPreview,
|
|
1988
|
+
extractedFromEnvelope,
|
|
1989
|
+
extractedJsonObject,
|
|
1990
|
+
lineCount: lines.length,
|
|
1991
|
+
lastNonEmptyLinePreview,
|
|
1992
|
+
rawOutputPreview,
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
if (parsed && typeof parsed === "object" && (parsed.kind || parsed.action || parsed.content || parsed.answerId)) {
|
|
1996
|
+
extractedJsonObject = true;
|
|
1997
|
+
return {
|
|
1998
|
+
text: jsonCandidates[i],
|
|
1999
|
+
jsonCandidateCount: jsonCandidates.length,
|
|
2000
|
+
envelopeDetected,
|
|
2001
|
+
envelopePayloadCount,
|
|
2002
|
+
envelopePayloadTextCount,
|
|
2003
|
+
envelopePayloadTextLength,
|
|
2004
|
+
envelopePayloadPreview,
|
|
2005
|
+
extractedFromEnvelope,
|
|
2006
|
+
extractedJsonObject,
|
|
2007
|
+
lineCount: lines.length,
|
|
2008
|
+
lastNonEmptyLinePreview,
|
|
2009
|
+
rawOutputPreview,
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
1784
2012
|
} catch {}
|
|
1785
2013
|
}
|
|
1786
|
-
|
|
1787
|
-
if (!lines.length)
|
|
2014
|
+
|
|
2015
|
+
if (!lines.length) {
|
|
2016
|
+
return {
|
|
2017
|
+
text: "",
|
|
2018
|
+
jsonCandidateCount: jsonCandidates.length,
|
|
2019
|
+
envelopeDetected,
|
|
2020
|
+
envelopePayloadCount,
|
|
2021
|
+
envelopePayloadTextCount,
|
|
2022
|
+
envelopePayloadTextLength,
|
|
2023
|
+
envelopePayloadPreview,
|
|
2024
|
+
extractedFromEnvelope,
|
|
2025
|
+
extractedJsonObject,
|
|
2026
|
+
lineCount: 0,
|
|
2027
|
+
lastNonEmptyLinePreview: "",
|
|
2028
|
+
rawOutputPreview,
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1788
2032
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
1789
2033
|
const line = lines[i];
|
|
1790
2034
|
if (!line) continue;
|
|
1791
|
-
if (line.startsWith("{") && line.endsWith("}"))
|
|
2035
|
+
if (line.startsWith("{") && line.endsWith("}")) {
|
|
2036
|
+
return {
|
|
2037
|
+
text: line,
|
|
2038
|
+
jsonCandidateCount: jsonCandidates.length,
|
|
2039
|
+
envelopeDetected,
|
|
2040
|
+
envelopePayloadCount,
|
|
2041
|
+
envelopePayloadTextCount,
|
|
2042
|
+
envelopePayloadTextLength,
|
|
2043
|
+
envelopePayloadPreview,
|
|
2044
|
+
extractedFromEnvelope,
|
|
2045
|
+
extractedJsonObject,
|
|
2046
|
+
lineCount: lines.length,
|
|
2047
|
+
lastNonEmptyLinePreview,
|
|
2048
|
+
rawOutputPreview,
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
1792
2051
|
}
|
|
1793
|
-
|
|
2052
|
+
|
|
2053
|
+
return {
|
|
2054
|
+
text: lines.join("\n"),
|
|
2055
|
+
jsonCandidateCount: jsonCandidates.length,
|
|
2056
|
+
envelopeDetected,
|
|
2057
|
+
envelopePayloadCount,
|
|
2058
|
+
envelopePayloadTextCount,
|
|
2059
|
+
envelopePayloadTextLength,
|
|
2060
|
+
envelopePayloadPreview,
|
|
2061
|
+
extractedFromEnvelope,
|
|
2062
|
+
extractedJsonObject,
|
|
2063
|
+
lineCount: lines.length,
|
|
2064
|
+
lastNonEmptyLinePreview,
|
|
2065
|
+
rawOutputPreview,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function buildAgentTurnResult(input: {
|
|
2070
|
+
stdout: string;
|
|
2071
|
+
stderr: string;
|
|
2072
|
+
sessionId: string;
|
|
2073
|
+
transport: "powershell" | "openclaw";
|
|
2074
|
+
durationMs: number;
|
|
2075
|
+
}): AgentTurnResult {
|
|
2076
|
+
const inspected = inspectAgentOutput(input.stdout);
|
|
2077
|
+
return {
|
|
2078
|
+
stdout: input.stdout,
|
|
2079
|
+
stderr: input.stderr,
|
|
2080
|
+
extractedText: inspected.text,
|
|
2081
|
+
diagnostics: {
|
|
2082
|
+
...inspected,
|
|
2083
|
+
sessionId: input.sessionId,
|
|
2084
|
+
transport: input.transport,
|
|
2085
|
+
durationMs: Math.max(0, Math.round(Number(input.durationMs) || 0)),
|
|
2086
|
+
stdoutLength: input.stdout.length,
|
|
2087
|
+
stderrLength: input.stderr.length,
|
|
2088
|
+
stderrPreview: compactPreview(input.stderr, 180),
|
|
2089
|
+
},
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function inspectAgentEnvelope(parsed: any) {
|
|
2094
|
+
const hasEnvelope = Array.isArray(parsed?.result?.payloads) || Array.isArray(parsed?.payloads);
|
|
2095
|
+
const payloads = Array.isArray(parsed?.result?.payloads)
|
|
2096
|
+
? parsed.result.payloads
|
|
2097
|
+
: Array.isArray(parsed?.payloads)
|
|
2098
|
+
? parsed.payloads
|
|
2099
|
+
: [];
|
|
2100
|
+
const payloadTexts = payloads
|
|
2101
|
+
.map((item: any) => (typeof item?.text === "string" ? item.text.trim() : ""))
|
|
2102
|
+
.filter(Boolean);
|
|
2103
|
+
return {
|
|
2104
|
+
detected: hasEnvelope,
|
|
2105
|
+
payloadCount: payloads.length,
|
|
2106
|
+
payloadTextCount: payloadTexts.length,
|
|
2107
|
+
payloadTextLength: payloadTexts.reduce((sum: number, item: string) => sum + item.length, 0),
|
|
2108
|
+
payloadPreview: compactPreview(payloadTexts.join("\n"), 180),
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function formatAgentTurnDiagnostics(diag: AgentTurnDiagnostics, reason: TaskFailureReason, outputText = "") {
|
|
2113
|
+
const parts = [
|
|
2114
|
+
`reason=${String(reason.code ?? "").trim() || "unknown"}`,
|
|
2115
|
+
`session=${diag.sessionId || "-"}`,
|
|
2116
|
+
`transport=${diag.transport}`,
|
|
2117
|
+
`durationMs=${diag.durationMs}`,
|
|
2118
|
+
`stdoutLen=${diag.stdoutLength}`,
|
|
2119
|
+
`stderrLen=${diag.stderrLength}`,
|
|
2120
|
+
`jsonCandidates=${diag.jsonCandidateCount}`,
|
|
2121
|
+
`envelope=${diag.envelopeDetected ? "yes" : "no"}`,
|
|
2122
|
+
`payloads=${diag.envelopePayloadCount}`,
|
|
2123
|
+
`payloadTexts=${diag.envelopePayloadTextCount}`,
|
|
2124
|
+
`payloadTextLen=${diag.envelopePayloadTextLength}`,
|
|
2125
|
+
`lineCount=${diag.lineCount}`,
|
|
2126
|
+
];
|
|
2127
|
+
if (diag.extractedFromEnvelope) parts.push("source=envelope");
|
|
2128
|
+
else if (diag.extractedJsonObject) parts.push("source=json-object");
|
|
2129
|
+
if (diag.rawOutputPreview) parts.push(`stdoutPreview="${diag.rawOutputPreview}"`);
|
|
2130
|
+
if (diag.envelopePayloadPreview) parts.push(`payloadPreview="${diag.envelopePayloadPreview}"`);
|
|
2131
|
+
if (diag.lastNonEmptyLinePreview) parts.push(`lastLine="${diag.lastNonEmptyLinePreview}"`);
|
|
2132
|
+
if (diag.stderrPreview) parts.push(`stderrPreview="${diag.stderrPreview}"`);
|
|
2133
|
+
if (outputText) parts.push(`extractedPreview="${compactPreview(outputText, 140)}"`);
|
|
2134
|
+
return parts.join(", ");
|
|
1794
2135
|
}
|
|
1795
2136
|
|
|
1796
2137
|
function extractPayloadTextFromAgentEnvelope(parsed: any): string {
|
|
@@ -2532,6 +2873,12 @@ function truncate(value: string, max: number) {
|
|
|
2532
2873
|
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
2533
2874
|
}
|
|
2534
2875
|
|
|
2876
|
+
function compactPreview(value: string, max = 180) {
|
|
2877
|
+
const cleaned = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
2878
|
+
if (!cleaned) return "";
|
|
2879
|
+
return truncate(cleaned, max);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2535
2882
|
function coerceToString(value: unknown): string {
|
|
2536
2883
|
if (typeof value === "string") return value;
|
|
2537
2884
|
if (value === null || value === undefined) return "";
|