























@@ -104,17 +104,7 @@ export function readSessionMessages(
104104}
105105106106const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
107-const hasTreeEntries = lines.some((line) => {
108-if (!line.trim()) {
109-return false;
110-}
111-try {
112-const parsed = JSON.parse(line) as { type?: unknown; id?: unknown; parentId?: unknown };
113-return parsed.type !== "session" && typeof parsed.id === "string" && "parentId" in parsed;
114-} catch {
115-return false;
116-}
117-});
107+const hasTreeEntries = lines.some(hasSessionTreeEntry);
118108let branchEntries: SessionEntry[] | null = null;
119109if (hasTreeEntries) {
120110try {
@@ -166,33 +156,10 @@ export function readSessionMessages(
166156}
167157try {
168158const parsed = JSON.parse(line);
169-if (parsed?.message) {
159+const message = parsedSessionEntryToMessage(parsed, messageSeq + 1);
160+if (message) {
170161messageSeq += 1;
171-messages.push(
172-attachOpenClawTranscriptMeta(parsed.message, {
173- ...(typeof parsed.id === "string" ? { id: parsed.id } : {}),
174-seq: messageSeq,
175-}),
176-);
177-continue;
178-}
179-180-// Compaction entries are not "message" records, but they're useful context for debugging.
181-// Emit a lightweight synthetic message that the Web UI can render as a divider.
182-if (parsed?.type === "compaction") {
183-const ts = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : Number.NaN;
184-const timestamp = Number.isFinite(ts) ? ts : Date.now();
185-messageSeq += 1;
186-messages.push({
187-role: "system",
188-content: [{ type: "text", text: "Compaction" }],
189- timestamp,
190-__openclaw: {
191-kind: "compaction",
192-id: typeof parsed.id === "string" ? parsed.id : undefined,
193-seq: messageSeq,
194-},
195-});
162+messages.push(message);
196163}
197164} catch {
198165// ignore bad lines
@@ -201,6 +168,128 @@ export function readSessionMessages(
201168return messages;
202169}
203170171+export type ReadRecentSessionMessagesOptions = {
172+maxMessages: number;
173+maxBytes?: number;
174+maxLines?: number;
175+};
176+177+const RECENT_SESSION_MESSAGES_DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
178+179+export function readRecentSessionMessages(
180+sessionId: string,
181+storePath: string | undefined,
182+sessionFile?: string,
183+opts?: ReadRecentSessionMessagesOptions,
184+): unknown[] {
185+const maxMessages = Math.max(0, Math.floor(opts?.maxMessages ?? 0));
186+if (maxMessages === 0) {
187+return [];
188+}
189+190+const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile);
191+if (!filePath) {
192+return [];
193+}
194+195+let stat: fs.Stats;
196+try {
197+stat = fs.statSync(filePath);
198+} catch {
199+return [];
200+}
201+if (stat.size === 0) {
202+return [];
203+}
204+205+const maxBytes = Math.max(
206+1024,
207+Math.floor(opts?.maxBytes ?? RECENT_SESSION_MESSAGES_DEFAULT_MAX_BYTES),
208+);
209+const readLen = Math.min(stat.size, maxBytes);
210+const readStart = Math.max(0, stat.size - readLen);
211+const maxLines = Math.max(maxMessages, Math.floor(opts?.maxLines ?? maxMessages * 20 + 20));
212+213+return (
214+withOpenTranscriptFd(filePath, (fd) => {
215+const buf = Buffer.alloc(readLen);
216+const bytesRead = fs.readSync(fd, buf, 0, readLen, readStart);
217+if (bytesRead <= 0) {
218+return [];
219+}
220+const chunk = buf.toString("utf-8", 0, bytesRead);
221+const lines = chunk
222+.split(/\r?\n/)
223+.slice(readStart > 0 ? 1 : 0)
224+.filter((line) => line.trim().length > 0)
225+.slice(-maxLines);
226+227+if (lines.some(hasSessionTreeEntry)) {
228+return readSessionMessages(sessionId, storePath, sessionFile).slice(-maxMessages);
229+}
230+231+const messages: unknown[] = [];
232+let messageSeq = 0;
233+for (const line of lines) {
234+try {
235+const parsed = JSON.parse(line);
236+const message = parsedSessionEntryToMessage(parsed, messageSeq + 1);
237+if (message) {
238+messageSeq += 1;
239+messages.push(message);
240+}
241+} catch {
242+// ignore bad tail lines
243+}
244+}
245+return messages.slice(-maxMessages);
246+}) ?? []
247+);
248+}
249+250+function hasSessionTreeEntry(line: string): boolean {
251+if (!line.trim()) {
252+return false;
253+}
254+try {
255+const parsed = JSON.parse(line) as { type?: unknown; id?: unknown; parentId?: unknown };
256+return parsed.type !== "session" && typeof parsed.id === "string" && "parentId" in parsed;
257+} catch {
258+return false;
259+}
260+}
261+262+function parsedSessionEntryToMessage(parsed: unknown, seq: number): unknown | null {
263+if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
264+return null;
265+}
266+const entry = parsed as Record<string, unknown>;
267+if (entry.message) {
268+return attachOpenClawTranscriptMeta(entry.message, {
269+ ...(typeof entry.id === "string" ? { id: entry.id } : {}),
270+ seq,
271+});
272+}
273+274+// Compaction entries are not "message" records, but they're useful context for debugging.
275+// Emit a lightweight synthetic message that the Web UI can render as a divider.
276+if (entry.type === "compaction") {
277+const ts = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
278+const timestamp = Number.isFinite(ts) ? ts : Date.now();
279+return {
280+role: "system",
281+content: [{ type: "text", text: "Compaction" }],
282+ timestamp,
283+__openclaw: {
284+kind: "compaction",
285+id: typeof entry.id === "string" ? entry.id : undefined,
286+ seq,
287+},
288+};
289+}
290+return null;
291+}
292+204293export {
205294archiveFileOnDisk,
206295archiveSessionTranscripts,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。