openclaw-clawtown-plugin 1.1.18 → 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.18",
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.18",
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;
@@ -640,6 +671,19 @@ class Reporter {
640
671
  let instructions = baseInstructions;
641
672
  let rawOutput = "";
642
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
+ };
643
687
  const reportClientFailure = async (reason: TaskFailureReason, outputText: string) => {
644
688
  const kind = actionKindForTaskType(taskType);
645
689
  const failureMeta = buildClientFailureMeta(taskType, outputText);
@@ -658,14 +702,31 @@ class Reporter {
658
702
  modelOutputPreview: failureMeta.modelOutputPreview,
659
703
  candidateAnswerLength: failureMeta.candidateAnswerLength,
660
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 ?? "",
661
719
  },
662
720
  rawOutput: outputText,
663
721
  });
664
722
  };
665
723
  try {
666
- rawOutput = extractReplyText(await this.runVisibleAgentTurn(instructions, TASK_FIRST_TURN_TIMEOUT_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
667
- taskKey: tid || qid || taskType,
668
- }));
724
+ rawOutput = await runTaskTurn(
725
+ instructions,
726
+ TASK_FIRST_TURN_TIMEOUT_SECONDS,
727
+ ACTION_MODEL_TIMEOUT_MS,
728
+ tid || qid || taskType,
729
+ );
669
730
  } catch (error: any) {
670
731
  executeErrorMessage = String(error?.message ?? error ?? "unknown");
671
732
  console.warn(`[forum-reporter-v2] execute task failed: ${executeErrorMessage}`);
@@ -674,6 +735,7 @@ class Reporter {
674
735
  let normalized = normalizeTaskResult(taskType, rawOutput, payload);
675
736
  if (!normalized) {
676
737
  const firstFailureReason = diagnoseTaskResultFailure(taskType, rawOutput, payload, executeErrorMessage);
738
+ logTaskFailureDiagnostics("first-attempt", firstFailureReason, rawOutput);
677
739
  if (firstFailureReason.code === "model_turn_failed") {
678
740
  const timeoutFeedback = humanizeFailureReason(firstFailureReason, taskType);
679
741
  console.warn(`[forum-reporter-v2] first attempt timeout for taskType=${taskType}, retry once with longer timeout`);
@@ -681,9 +743,12 @@ class Reporter {
681
743
  rawOutput = "";
682
744
  executeErrorMessage = "";
683
745
  try {
684
- rawOutput = extractReplyText(await this.runVisibleAgentTurn(instructions, TASK_TIMEOUT_RETRY_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
685
- taskKey: `${tid || qid || taskType}-timeout-retry1`,
686
- }));
746
+ rawOutput = await runTaskTurn(
747
+ instructions,
748
+ TASK_TIMEOUT_RETRY_SECONDS,
749
+ ACTION_MODEL_TIMEOUT_MS,
750
+ `${tid || qid || taskType}-timeout-retry1`,
751
+ );
687
752
  } catch (error: any) {
688
753
  executeErrorMessage = String(error?.message ?? error ?? "unknown");
689
754
  console.warn(`[forum-reporter-v2] timeout-retry execute failed: ${executeErrorMessage}`);
@@ -691,6 +756,7 @@ class Reporter {
691
756
  normalized = normalizeTaskResult(taskType, rawOutput, payload);
692
757
  if (!normalized) {
693
758
  const timeoutRetryFailure = diagnoseTaskResultFailure(taskType, rawOutput, payload, executeErrorMessage);
759
+ logTaskFailureDiagnostics("timeout-retry", timeoutRetryFailure, rawOutput);
694
760
  await reportClientFailure(timeoutRetryFailure, rawOutput);
695
761
  return;
696
762
  }
@@ -702,9 +768,12 @@ class Reporter {
702
768
  rawOutput = "";
703
769
  executeErrorMessage = "";
704
770
  try {
705
- rawOutput = extractReplyText(await this.runVisibleAgentTurn(instructions, TASK_FIRST_TURN_TIMEOUT_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
706
- taskKey: `${tid || qid || taskType}-retry1`,
707
- }));
771
+ rawOutput = await runTaskTurn(
772
+ instructions,
773
+ TASK_FIRST_TURN_TIMEOUT_SECONDS,
774
+ ACTION_MODEL_TIMEOUT_MS,
775
+ `${tid || qid || taskType}-retry1`,
776
+ );
708
777
  } catch (error: any) {
709
778
  executeErrorMessage = String(error?.message ?? error ?? "unknown");
710
779
  console.warn(`[forum-reporter-v2] retry execute failed: ${executeErrorMessage}`);
@@ -712,6 +781,7 @@ class Reporter {
712
781
  normalized = normalizeTaskResult(taskType, rawOutput, payload);
713
782
  if (!normalized) {
714
783
  const failureReason = diagnoseTaskResultFailure(taskType, rawOutput, payload, executeErrorMessage);
784
+ logTaskFailureDiagnostics("retry1", failureReason, rawOutput);
715
785
  await reportClientFailure(failureReason, rawOutput);
716
786
  return;
717
787
  }
@@ -732,9 +802,12 @@ class Reporter {
732
802
  const retryInstructions = buildRetryInstructions(baseInstructions, serverFeedback);
733
803
  let retryOutput = "";
734
804
  try {
735
- retryOutput = extractReplyText(await this.runVisibleAgentTurn(retryInstructions, TASK_FIRST_TURN_TIMEOUT_SECONDS, ACTION_MODEL_TIMEOUT_MS, {
736
- taskKey: `${tid || qid || taskType}-submit-retry1`,
737
- }));
805
+ retryOutput = await runTaskTurn(
806
+ retryInstructions,
807
+ TASK_FIRST_TURN_TIMEOUT_SECONDS,
808
+ ACTION_MODEL_TIMEOUT_MS,
809
+ `${tid || qid || taskType}-submit-retry1`,
810
+ );
738
811
  } catch (error: any) {
739
812
  const msg = String(error?.message ?? error ?? "unknown");
740
813
  console.warn(`[forum-reporter-v2] submit-retry execute failed: ${msg}`);
@@ -743,6 +816,7 @@ class Reporter {
743
816
  const retryNormalized = normalizeTaskResult(taskType, retryOutput, payload);
744
817
  if (!retryNormalized) {
745
818
  const retryFailure = diagnoseTaskResultFailure(taskType, retryOutput, payload, "");
819
+ logTaskFailureDiagnostics("submit-retry", retryFailure, retryOutput);
746
820
  console.warn(`[forum-reporter-v2] submit-retry produced invalid result: ${retryFailure.code}`);
747
821
  return;
748
822
  }
@@ -990,10 +1064,19 @@ class Reporter {
990
1064
  const prompt = buildV2ActionPrompt(context);
991
1065
  let text = "";
992
1066
  try {
993
- const stdout = await this.runVisibleAgentTurn(prompt, 115, ACTION_CONTEXT_TIMEOUT_MS, {
1067
+ const turn = await this.runVisibleAgentTurn(prompt, 115, ACTION_CONTEXT_TIMEOUT_MS, {
994
1068
  taskKey: `context-${context.userId}`,
995
1069
  });
996
- 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
+ }
997
1080
  } catch (error: any) {
998
1081
  const modelTurnError = String(error?.message ?? error ?? "unknown");
999
1082
  console.warn(`[forum-reporter-v2] model turn failed, fallback to deterministic action: ${modelTurnError}`);
@@ -1039,7 +1122,7 @@ class Reporter {
1039
1122
  timeoutSeconds: number,
1040
1123
  timeoutMs: number,
1041
1124
  options?: { taskKey?: string },
1042
- ): Promise<string> {
1125
+ ): Promise<AgentTurnResult> {
1043
1126
  // 每次任务使用一次性 session,避免历史累积污染后续任务。
1044
1127
  const baseSessionId = buildOneShotSessionId(this.userId, options?.taskKey);
1045
1128
  try {
@@ -1066,8 +1149,9 @@ class Reporter {
1066
1149
  message: string,
1067
1150
  timeoutSeconds: number,
1068
1151
  timeoutMs: number,
1069
- ): Promise<string> {
1152
+ ): Promise<AgentTurnResult> {
1070
1153
  const isWindows = process.platform === "win32";
1154
+ const startedAt = Date.now();
1071
1155
  if (isWindows) {
1072
1156
  const script = buildWindowsAgentCommandScript({
1073
1157
  agentId: this.openclawAgentId,
@@ -1096,7 +1180,13 @@ class Reporter {
1096
1180
  }),
1097
1181
  timeoutMs,
1098
1182
  );
1099
- 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
+ });
1100
1190
  }
1101
1191
 
1102
1192
  const args = [
@@ -1122,7 +1212,13 @@ class Reporter {
1122
1212
  }),
1123
1213
  timeoutMs,
1124
1214
  );
1125
- 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
+ });
1126
1222
  }
1127
1223
 
1128
1224
  private resolveInstanceLockPath(userId: string) {
@@ -1832,25 +1928,210 @@ function quoteForPowerShell(value: string) {
1832
1928
  }
1833
1929
 
1834
1930
  function extractReplyText(output: string): string {
1835
- const text = String(output ?? "").trim();
1836
- if (!text) return "";
1837
- 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
+
1838
1966
  for (let i = jsonCandidates.length - 1; i >= 0; i -= 1) {
1839
1967
  try {
1840
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
+ }
1841
1977
  const payloadText = extractPayloadTextFromAgentEnvelope(parsed);
1842
- if (payloadText) return payloadText;
1843
- 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
+ }
1844
2012
  } catch {}
1845
2013
  }
1846
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1847
- 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
+
1848
2032
  for (let i = lines.length - 1; i >= 0; i -= 1) {
1849
2033
  const line = lines[i];
1850
2034
  if (!line) continue;
1851
- 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
+ }
1852
2051
  }
1853
- 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(", ");
1854
2135
  }
1855
2136
 
1856
2137
  function extractPayloadTextFromAgentEnvelope(parsed: any): string {
@@ -2592,6 +2873,12 @@ function truncate(value: string, max: number) {
2592
2873
  return `${value.slice(0, Math.max(0, max - 1))}…`;
2593
2874
  }
2594
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
+
2595
2882
  function coerceToString(value: unknown): string {
2596
2883
  if (typeof value === "string") return value;
2597
2884
  if (value === null || value === undefined) return "";