惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
Y
Y Combinator Blog
I
InfoQ
Recent Announcements
Recent Announcements
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
IT之家
IT之家
H
Help Net Security
月光博客
月光博客
S
SegmentFault 最新的问题
B
Blog
aimingoo的专栏
aimingoo的专栏
GbyAI
GbyAI
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
G
Google Developers Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Vercel News
Vercel News
博客园 - 叶小钗
Microsoft Security Blog
Microsoft Security Blog
腾讯CDC
Jina AI
Jina AI
T
The Blog of Author Tim Ferriss

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(gateway): expose idempotencyKey in chat history metad...
wyf027 · 2026-06-29 · via Recent Commits to openclaw:main

@@ -31,6 +31,7 @@ import {

3131

extractJsonNullableStringFieldPrefix,

3232

extractJsonNumberFieldPrefix,

3333

extractJsonStringFieldPrefix,

34+

normalizeOptionalString,

3435

} from "./session-transcript-json.js";

3536

import 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. */

162171

export function readSessionMessages(

163172

sessionId: string,

@@ -301,12 +310,60 @@ async function readRecentTranscriptTailLinesAsync(

301310302311

const MAX_TRANSCRIPT_PARSE_LINE_BYTES = 256 * 1024;

303312

const OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS = 64 * 1024;

313+

const OVERSIZED_TRANSCRIPT_METADATA_SUFFIX_CHARS = 64 * 1024;

304314

const TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER = "[chat.history omitted: message too large]";

305315306316

function isOversizedTranscriptLine(line: string): boolean {

307317

return 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+310367

function buildOversizedTranscriptRecord(line: string): TailTranscriptRecord {

311368

const prefix = line.slice(0, OVERSIZED_TRANSCRIPT_METADATA_PREFIX_CHARS);

312369

const messageMatch = /"message"\s*:/.exec(prefix);

@@ -318,13 +375,17 @@ function buildOversizedTranscriptRecord(line: string): TailTranscriptRecord {

318375

extractJsonStringFieldPrefix(recordPrefix, "timestamp") ??

319376

extractJsonNumberFieldPrefix(recordPrefix, "timestamp");

320377

const role = extractJsonStringFieldPrefix(prefix, "role") ?? "assistant";

378+

const idempotencyKey =

379+

extractJsonStringFieldPrefix(prefix, "idempotencyKey") ??

380+

extractJsonStringFieldSuffix(line, "idempotencyKey");

321381

const record: Record<string, unknown> = {

322382

...(type ? { type } : {}),

323383

...(id ? { id } : {}),

324384

...(parentId !== undefined ? { parentId } : {}),

325385

...(timestamp !== undefined ? { timestamp } : {}),

326386

message: {

327387

role,

388+

...(idempotencyKey ? { idempotencyKey } : {}),

328389

content: [{ 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);

841903

return attachOpenClawTranscriptMeta(entry.message, {

842904

...(typeof entry.id === "string" ? { id: entry.id } : {}),

905+

...(idempotencyKey ? { idempotencyKey } : {}),

843906

...(Number.isFinite(recordTimestampMs) ? { recordTimestampMs } : {}),

844907

seq,

845908

});