






















@@ -1,9 +1,9 @@
1-import type { ChatItem, MessageGroup, ToolCard } from "../types/chat-types.ts";
1+import type { ChatItem, MessageGroup, NormalizedMessage, ToolCard } from "../types/chat-types.ts";
22import {
33isAssistantHeartbeatAckForDisplay,
44stripHeartbeatTokenForDisplay,
55} from "./heartbeat-display.ts";
6-import { CHAT_HISTORY_RENDER_LIMIT } from "./history-limits.ts";
6+import { CHAT_HISTORY_RENDER_CHAR_BUDGET, CHAT_HISTORY_RENDER_LIMIT } from "./history-limits.ts";
77import { extractTextCached } from "./message-extract.ts";
88import { normalizeMessage, stripMessageDisplayMetadataText } from "./message-normalizer.ts";
99import { normalizeRoleForGrouping } from "./role-normalizer.ts";
@@ -66,12 +66,32 @@ function appendCanvasBlockToAssistantMessage(
6666};
6767}
686869+function asRecord(value: unknown): Record<string, unknown> | null {
70+return value && typeof value === "object" && !Array.isArray(value)
71+ ? (value as Record<string, unknown>)
72+ : null;
73+}
74+75+function safeNormalizeMessage(message: unknown): NormalizedMessage | null {
76+if (!asRecord(message)) {
77+return null;
78+}
79+try {
80+return normalizeMessage(message);
81+} catch {
82+return null;
83+}
84+}
85+6986function extractChatMessagePreview(toolMessage: unknown): {
7087preview: Extract<NonNullable<ToolCard["preview"]>, { kind: "canvas" }>;
7188text: string | null;
7289timestamp: number | null;
7390} | null {
74-const normalized = normalizeMessage(toolMessage);
91+const normalized = safeNormalizeMessage(toolMessage);
92+if (!normalized) {
93+return null;
94+}
7595const cards = extractToolCards(toolMessage, "preview");
7696for (let index = cards.length - 1; index >= 0; index--) {
7797const card = cards[index];
@@ -114,7 +134,7 @@ function findNearestAssistantMessageIndex(
114134}
115135return {
116136 index,
117-timestamp: normalizeMessage(item.message).timestamp ?? null,
137+timestamp: safeNormalizeMessage(item.message)?.timestamp ?? null,
118138};
119139})
120140.filter(Boolean) as Array<{ index: number; timestamp: number | null }>;
@@ -203,7 +223,10 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
203223}
204224205225function collapseDuplicateDisplaySignature(message: unknown): string | null {
206-const normalized = normalizeMessage(message);
226+const normalized = safeNormalizeMessage(message);
227+if (!normalized) {
228+return null;
229+}
207230const role = normalizeRoleForGrouping(normalized.role).toLowerCase();
208231if (!role || role === "tool") {
209232return null;
@@ -250,7 +273,10 @@ function collapseSequentialDuplicateMessages(items: ChatItem[]): ChatItem[] {
250273}
251274252275function hasRenderableNormalizedMessage(message: unknown): boolean {
253-const normalized = normalizeMessage(message);
276+const normalized = safeNormalizeMessage(message);
277+if (!normalized) {
278+return false;
279+}
254280return normalized.content.length > 0 || Boolean(normalized.replyTarget);
255281}
256282@@ -260,7 +286,7 @@ function sanitizeStreamText(text: string): string {
260286}
261287262288function rawMessageTimestamp(message: unknown): number | null {
263-const timestamp = (message as { timestamp?: unknown }).timestamp;
289+const timestamp = asRecord(message)?.timestamp;
264290return typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : null;
265291}
266292@@ -301,6 +327,129 @@ function sortChatItemsByVisibleTime(items: ChatItem[]): ChatItem[] {
301327.map(({ item }) => item);
302328}
303329330+type RawContentEstimateState = {
331+visited: WeakSet<object>;
332+nodes: number;
333+};
334+335+const RAW_CONTENT_ESTIMATE_MAX_DEPTH = 8;
336+const RAW_CONTENT_ESTIMATE_MAX_NODES = 400;
337+338+function addCapped(total: number, amount: number, limit: number): number {
339+return Math.min(limit, total + Math.max(0, amount));
340+}
341+342+function estimateRawContentChars(
343+value: unknown,
344+limit: number,
345+state: RawContentEstimateState,
346+depth = 0,
347+): number {
348+if (limit <= 0) {
349+return 0;
350+}
351+if (typeof value === "string") {
352+return Math.min(value.length, limit);
353+}
354+if (!value || typeof value !== "object") {
355+return 0;
356+}
357+if (depth >= RAW_CONTENT_ESTIMATE_MAX_DEPTH || state.nodes >= RAW_CONTENT_ESTIMATE_MAX_NODES) {
358+return 0;
359+}
360+if (state.visited.has(value)) {
361+return 0;
362+}
363+state.visited.add(value);
364+state.nodes += 1;
365+366+if (Array.isArray(value)) {
367+let chars = 0;
368+for (const item of value) {
369+chars = addCapped(
370+chars,
371+estimateRawContentChars(item, limit - chars, state, depth + 1),
372+limit,
373+);
374+if (chars >= limit) {
375+break;
376+}
377+}
378+return chars;
379+}
380+381+const record = value as Record<string, unknown>;
382+let chars = 0;
383+for (const key of ["text", "content", "args", "arguments", "input"] as const) {
384+chars = addCapped(
385+chars,
386+estimateRawContentChars(record[key], limit - chars, state, depth + 1),
387+limit,
388+);
389+if (chars >= limit) {
390+break;
391+}
392+}
393+return chars;
394+}
395+396+function estimateMessageRenderChars(message: unknown, limit: number): number {
397+const record = asRecord(message);
398+if (!record) {
399+return 1;
400+}
401+const state: RawContentEstimateState = { visited: new WeakSet<object>(), nodes: 0 };
402+let chars = 0;
403+for (const key of ["content", "text", "args", "arguments", "input"] as const) {
404+chars = addCapped(chars, estimateRawContentChars(record[key], limit - chars, state), limit);
405+if (chars >= limit) {
406+break;
407+}
408+}
409+return Math.max(chars, 1);
410+}
411+412+function isHiddenToolMessage(message: unknown, showToolCalls: boolean): boolean {
413+if (showToolCalls) {
414+return false;
415+}
416+return safeNormalizeMessage(message)?.role.toLowerCase() === "toolresult";
417+}
418+419+function countVisibleHistoryMessages(messages: unknown[], showToolCalls: boolean): number {
420+let count = 0;
421+for (const message of messages) {
422+if (!isHiddenToolMessage(message, showToolCalls)) {
423+count += 1;
424+}
425+}
426+return count;
427+}
428+429+function resolveHistoryStartIndex(messages: unknown[], showToolCalls: boolean): number {
430+let visibleCount = 0;
431+let renderChars = 0;
432+let startIndex = messages.length;
433+for (let index = messages.length - 1; index >= 0; index -= 1) {
434+const message = messages[index];
435+if (isHiddenToolMessage(message, showToolCalls)) {
436+continue;
437+}
438+if (visibleCount >= CHAT_HISTORY_RENDER_LIMIT) {
439+break;
440+}
441+const remainingBudget = Math.max(1, CHAT_HISTORY_RENDER_CHAR_BUDGET - renderChars + 1);
442+const messageChars = estimateMessageRenderChars(message, remainingBudget);
443+if (visibleCount > 0 && renderChars + messageChars > CHAT_HISTORY_RENDER_CHAR_BUDGET) {
444+break;
445+}
446+renderChars += messageChars;
447+visibleCount += 1;
448+startIndex = index;
449+}
450+return startIndex;
451+}
452+304453export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | MessageGroup> {
305454let items: ChatItem[] = [];
306455const history = (Array.isArray(props.messages) ? props.messages : []).filter(
@@ -314,22 +463,33 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
314463text: string | null;
315464timestamp: number | null;
316465}>;
317-const historyStart = Math.max(0, history.length - CHAT_HISTORY_RENDER_LIMIT);
318-if (historyStart > 0) {
466+const historyStart = resolveHistoryStartIndex(history, props.showToolCalls);
467+const hiddenHistoryCount = countVisibleHistoryMessages(
468+history.slice(0, historyStart),
469+props.showToolCalls,
470+);
471+const visibleHistoryCount = countVisibleHistoryMessages(
472+history.slice(historyStart),
473+props.showToolCalls,
474+);
475+if (hiddenHistoryCount > 0) {
319476items.push({
320477kind: "message",
321478key: "chat:history:notice",
322479message: {
323480role: "system",
324-content: `Showing last ${CHAT_HISTORY_RENDER_LIMIT} messages (${historyStart} hidden).`,
481+content: `Showing last ${visibleHistoryCount} messages (${hiddenHistoryCount} hidden).`,
325482timestamp: Date.now(),
326483},
327484});
328485}
329486for (let i = historyStart; i < history.length; i++) {
330487const msg = history[i];
331-const normalized = normalizeMessage(msg);
332-const raw = msg as Record<string, unknown>;
488+const normalized = safeNormalizeMessage(msg);
489+if (!normalized) {
490+continue;
491+}
492+const raw = asRecord(msg) ?? {};
333493const marker = raw.__openclaw as Record<string, unknown> | undefined;
334494if (marker && marker.kind === "compaction") {
335495items.push({
@@ -433,7 +593,7 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
433593}
434594435595function messageKey(message: unknown, index: number): string {
436-const m = message as Record<string, unknown>;
596+const m = asRecord(message) ?? {};
437597const toolCallId = typeof m.toolCallId === "string" ? m.toolCallId : "";
438598if (toolCallId) {
439599const role = typeof m.role === "string" ? m.role : "unknown";
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。