






























@@ -31,6 +31,7 @@ import {
3131extractJsonNullableStringFieldPrefix,
3232extractJsonNumberFieldPrefix,
3333extractJsonStringFieldPrefix,
34+normalizeOptionalString,
3435} from "./session-transcript-json.js";
3536import type { SessionPreviewItem } from "./session-utils.types.js";
3637@@ -158,6 +159,14 @@ export function attachOpenClawTranscriptMeta(
158159};
159160}
160161162+function readTranscriptMessageIdempotencyKey(message: unknown): string | undefined {
163+if (!message || typeof message !== "object" || Array.isArray(message)) {
164+return undefined;
165+}
166+const value = (message as Record<string, unknown>).idempotencyKey;
167+return typeof value === "string" && value.trim() ? value : undefined;
168+}
169+161170/** Read all visible transcript messages for a session from the first existing candidate file. */
162171export function readSessionMessages(
163172sessionId: string,
@@ -301,12 +310,60 @@ async function readRecentTranscriptTailLinesAsync(
301310302311const MAX_TRANSCRIPT_PARSE_LINE_BYTES = 256 * 1024;
303312const OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS = 64 * 1024;
313+const OVERSIZED_TRANSCRIPT_METADATA_SUFFIX_CHARS = 64 * 1024;
304314const TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER = "[chat.history omitted: message too large]";
305315306316function isOversizedTranscriptLine(line: string): boolean {
307317return Buffer.byteLength(line, "utf8") > MAX_TRANSCRIPT_PARSE_LINE_BYTES;
308318}
309319320+function isJsonObjectFieldToken(source: string, tokenIndex: number): boolean {
321+for (let index = tokenIndex - 1; index >= 0; index--) {
322+const char = source[index];
323+if (/\s/.test(char)) {
324+continue;
325+}
326+return char === "{" || char === ",";
327+}
328+return true;
329+}
330+331+function extractJsonStringFieldWindow(
332+source: string,
333+field: string,
334+startIndex = 0,
335+endIndex = source.length,
336+): string | undefined {
337+const fieldToken = JSON.stringify(field);
338+let searchIndex = startIndex;
339+while (searchIndex < endIndex) {
340+const tokenIndex = source.indexOf(fieldToken, searchIndex);
341+if (tokenIndex < 0 || tokenIndex >= endIndex) {
342+return undefined;
343+}
344+searchIndex = tokenIndex + fieldToken.length;
345+if (!isJsonObjectFieldToken(source, tokenIndex)) {
346+continue;
347+}
348+const match = /^\s*:\s*"((?:\\.|[^"\\])*)"/.exec(source.slice(searchIndex, endIndex));
349+if (!match) {
350+continue;
351+}
352+try {
353+const decoded = JSON.parse(`"${match[1]}"`) as unknown;
354+return normalizeOptionalString(decoded);
355+} catch {
356+return undefined;
357+}
358+}
359+return undefined;
360+}
361+362+function extractJsonStringFieldSuffix(source: string, field: string): string | undefined {
363+const startIndex = Math.max(0, source.length - OVERSIZED_TRANSCRIPT_METADATA_SUFFIX_CHARS);
364+return extractJsonStringFieldWindow(source, field, startIndex);
365+}
366+310367function buildOversizedTranscriptRecord(line: string): TailTranscriptRecord {
311368const prefix = line.slice(0, OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS);
312369const messageMatch = /"message"\s*:/.exec(prefix);
@@ -318,13 +375,17 @@ function buildOversizedTranscriptRecord(line: string): TailTranscriptRecord {
318375extractJsonStringFieldPrefix(recordPrefix, "timestamp") ??
319376extractJsonNumberFieldPrefix(recordPrefix, "timestamp");
320377const role = extractJsonStringFieldPrefix(prefix, "role") ?? "assistant";
378+const idempotencyKey =
379+extractJsonStringFieldPrefix(prefix, "idempotencyKey") ??
380+extractJsonStringFieldSuffix(line, "idempotencyKey");
321381const record: Record<string, unknown> = {
322382 ...(type ? { type } : {}),
323383 ...(id ? { id } : {}),
324384 ...(parentId !== undefined ? { parentId } : {}),
325385 ...(timestamp !== undefined ? { timestamp } : {}),
326386message: {
327387 role,
388+ ...(idempotencyKey ? { idempotencyKey } : {}),
328389content: [{ type: "text", text: TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER }],
329390__openclaw: { truncated: true, reason: "oversized" },
330391},
@@ -838,8 +899,10 @@ function parsedSessionEntryToMessage(parsed: unknown, seq: number): unknown {
838899 : typeof entry.timestamp === "number"
839900 ? entry.timestamp
840901 : Number.NaN;
902+const idempotencyKey = readTranscriptMessageIdempotencyKey(entry.message);
841903return attachOpenClawTranscriptMeta(entry.message, {
842904 ...(typeof entry.id === "string" ? { id: entry.id } : {}),
905+ ...(idempotencyKey ? { idempotencyKey } : {}),
843906 ...(Number.isFinite(recordTimestampMs) ? { recordTimestampMs } : {}),
844907 seq,
845908});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。