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.
@@ -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.17",
5
+ "version": "1.1.19",
6
6
  "main": "./index.ts",
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-clawtown-plugin",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Forum reporter plugin for OpenClaw Forum (Clawtown)",
5
5
  "license": "MIT",
6
6
  "main": "index.ts",
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
- await this.executeTask(task);
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 = extractReplyText(await this.runVisibleAgentTurn(instructions, TASK_FIRST_TURN_TIMEOUT_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
647
- taskKey: tid || qid || taskType,
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 = extractReplyText(await this.runVisibleAgentTurn(instructions, TASK_TIMEOUT_RETRY_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
665
- taskKey: `${tid || qid || taskType}-timeout-retry1`,
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 = extractReplyText(await this.runVisibleAgentTurn(instructions, TASK_FIRST_TURN_TIMEOUT_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
686
- taskKey: `${tid || qid || taskType}-retry1`,
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 = extractReplyText(await this.runVisibleAgentTurn(retryInstructions, TASK_FIRST_TURN_TIMEOUT_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
716
- taskKey: `${tid || qid || taskType}-submit-retry1`,
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
- console.warn(`[forum-reporter-v2] action-response failed: ${submitRes.status} ${rawText}${reasonCode ? ` (reasonCode=${reasonCode})` : ""}`);
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: String(body.error ?? "").trim() || undefined,
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 stdout = await this.runVisibleAgentTurn(prompt, 115, ACTION_CONTEXT_TIMEOUT_MS, {
1067
+ const turn = await this.runVisibleAgentTurn(prompt, 115, ACTION_CONTEXT_TIMEOUT_MS, {
968
1068
  taskKey: `context-${context.userId}`,
969
1069
  });
970
- text = extractReplyText(stdout);
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<string> {
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<string> {
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 String(result.stdout ?? "");
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 String(result.stdout ?? "");
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
- const text = String(output ?? "").trim();
1776
- if (!text) return "";
1777
- const jsonCandidates = parseJsonObjectCandidates(text);
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) return payloadText;
1783
- if (parsed && typeof parsed === "object" && (parsed.kind || parsed.action || parsed.content || parsed.answerId)) return jsonCandidates[i];
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
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1787
- if (!lines.length) return "";
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("}")) return line;
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
- return lines.join("\n");
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 "";