

























@@ -245,6 +245,54 @@ describe("SessionManager.open", () => {
245245}
246246});
247247248+it("publishes owned snapshots when a safe append pushes the transcript over the cache limit", async () => {
249+const dir = await makeTempDir();
250+const sessionFile = path.join(dir, "large-session.jsonl");
251+const maxCachedSessionBytes = 32 * 1024 * 1024;
252+const headerLine = JSON.stringify(buildSessionHeader(dir));
253+const largeEntryBase = {
254+type: "message",
255+id: "assistant-1",
256+parentId: null,
257+timestamp: "2026-06-04T00:00:01.000Z",
258+message: buildAssistantMessage(""),
259+};
260+const initialTranscriptWithContent = (content: string) =>
261+`${headerLine}\n${JSON.stringify({
262+ ...largeEntryBase,
263+ message: buildAssistantMessage(content),
264+ })}\n`;
265+let filler = "x".repeat(
266+maxCachedSessionBytes - Buffer.byteLength(initialTranscriptWithContent(""), "utf8") - 16,
267+);
268+while (
269+Buffer.byteLength(initialTranscriptWithContent(filler), "utf8") >
270+maxCachedSessionBytes - 16
271+) {
272+filler = filler.slice(0, -1024);
273+}
274+await fs.writeFile(sessionFile, initialTranscriptWithContent(filler), "utf8");
275+276+const sessionManager = SessionManager.open(sessionFile, dir, dir);
277+const publishSessionFileSnapshot = vi.fn(() => true);
278+await withOwnedSessionTranscriptWrites(
279+{
280+ sessionFile,
281+canAdvanceSessionEntryCache: () => true,
282+ publishSessionFileSnapshot,
283+withSessionWriteLock: async (run) => await run(),
284+},
285+async () => {
286+sessionManager.appendMessage(buildAssistantMessage("small append"));
287+},
288+);
289+290+expect(Buffer.byteLength(await fs.readFile(sessionFile, "utf8"), "utf8")).toBeGreaterThan(
291+maxCachedSessionBytes,
292+);
293+expect(publishSessionFileSnapshot).toHaveBeenCalledTimes(1);
294+});
295+248296it("invalidates warm entries after an append outside the owned write context", async () => {
249297const dir = await makeTempDir();
250298const sessionFile = path.join(dir, "session.jsonl");
@@ -370,7 +418,7 @@ describe("SessionManager.open", () => {
370418expect(warmEntry).toMatchObject({ data: { value: "first" } });
371419});
372420373-it("validates the transcript prefix after extension-owned entries are serialized", async () => {
421+it("validates the transcript prefix after entries with custom serializers are serialized", async () => {
374422const appenders: Array<{
375423name: string;
376424append: (manager: SessionManager, value: unknown) => void;
@@ -402,55 +450,144 @@ describe("SessionManager.open", () => {
402450append: (manager, value) =>
403451manager.branchWithSummary("assistant-1", "summary", { value }, true),
404452},
453+{
454+name: "tool_result_details",
455+append: (manager, value) =>
456+manager.appendMessage({
457+role: "toolResult",
458+toolCallId: "call-1",
459+toolName: "test",
460+content: [{ type: "text", text: "ok" }],
461+details: { value },
462+isError: false,
463+timestamp: Date.now(),
464+} as Parameters<SessionManager["appendMessage"]>[0]),
465+},
405466];
406467407-for (const { name, append } of appenders) {
408-const dir = await makeTempDir();
409-const sessionFile = path.join(dir, `${name}.jsonl`);
410-const originalEntry = {
411-type: "message",
412-id: "assistant-1",
413-parentId: null,
414-timestamp: "2026-06-04T00:00:01.000Z",
415-message: buildAssistantMessage("message 1"),
416-};
417-const replacementEntry = {
418- ...originalEntry,
419-message: buildAssistantMessage("changed 1"),
468+const serializerCases: Array<{
469+name: string;
470+createValue: (rewriteTranscript: () => void) => {
471+value: unknown;
472+cleanup?: () => void;
420473};
421-const headerLine = JSON.stringify(buildSessionHeader(dir));
422-await fs.writeFile(sessionFile, `${headerLine}\n${JSON.stringify(originalEntry)}\n`, "utf8");
423-424-const sessionManager = SessionManager.open(sessionFile, dir, dir);
425-const publishSessionFileSnapshot = vi.fn(() => true);
426-await withOwnedSessionTranscriptWrites(
427-{
428- sessionFile,
429-canAdvanceSessionEntryCache: () => true,
430- publishSessionFileSnapshot,
431-withSessionWriteLock: async (run) => await run(),
432-},
433-async () => {
434-append(sessionManager, {
474+}> = [
475+{
476+name: "own_to_json",
477+createValue: (rewriteTranscript) => ({
478+value: {
435479toJSON() {
436-writeFileSync(
437-sessionFile,
438-`${headerLine}\n${JSON.stringify(replacementEntry)}\n`,
439-"utf8",
440-);
480+rewriteTranscript();
441481return "persisted";
442482},
483+},
484+}),
485+},
486+{
487+name: "non_enumerable_array_index",
488+createValue: (rewriteTranscript) => {
489+const array = ["placeholder"];
490+Object.defineProperty(array, "0", {
491+configurable: true,
492+enumerable: false,
493+value: {
494+toJSON() {
495+rewriteTranscript();
496+return "persisted";
497+},
498+},
443499});
500+return { value: array };
444501},
445-);
502+},
503+{
504+name: "bigint_to_json",
505+createValue: (rewriteTranscript) => {
506+const originalBigIntToJson = Object.getOwnPropertyDescriptor(BigInt.prototype, "toJSON");
507+// eslint-disable-next-line no-extend-native -- JSON.stringify invokes BigInt.prototype.toJSON when present.
508+Object.defineProperty(BigInt.prototype, "toJSON", {
509+configurable: true,
510+value() {
511+rewriteTranscript();
512+return "persisted";
513+},
514+});
515+return {
516+value: 1n,
517+cleanup: () => {
518+if (originalBigIntToJson) {
519+// eslint-disable-next-line no-extend-native -- Restore the serializer installed for this case.
520+Object.defineProperty(BigInt.prototype, "toJSON", originalBigIntToJson);
521+} else {
522+delete (BigInt.prototype as { toJSON?: unknown }).toJSON;
523+}
524+},
525+};
526+},
527+},
528+];
446529447-expect(
448-SessionManager.open(sessionFile, dir, dir)
449-.getEntries()
450-.filter((entry) => entry.type === "message")
451-.map((entry) => readMessageContent(entry)),
452-).toEqual(["changed 1"]);
453-expect(publishSessionFileSnapshot).toHaveBeenCalledTimes(1);
530+for (const { name, append } of appenders) {
531+for (const serializerCase of serializerCases) {
532+const dir = await makeTempDir();
533+const sessionFile = path.join(dir, `${name}-${serializerCase.name}.jsonl`);
534+const originalEntry = {
535+type: "message",
536+id: "assistant-1",
537+parentId: null,
538+timestamp: "2026-06-04T00:00:01.000Z",
539+message: buildAssistantMessage("message 1"),
540+};
541+const replacementEntry = {
542+ ...originalEntry,
543+message: buildAssistantMessage("changed 1"),
544+};
545+const headerLine = JSON.stringify(buildSessionHeader(dir));
546+await fs.writeFile(
547+sessionFile,
548+`${headerLine}\n${JSON.stringify(originalEntry)}\n`,
549+"utf8",
550+);
551+552+const sessionManager = SessionManager.open(sessionFile, dir, dir);
553+let cacheAdvanceChecks = 0;
554+const publishSessionFileSnapshot = vi.fn(() => true);
555+const { value, cleanup } = serializerCase.createValue(() => {
556+writeFileSync(
557+sessionFile,
558+`${headerLine}\n${JSON.stringify(replacementEntry)}\n`,
559+"utf8",
560+);
561+});
562+563+try {
564+await withOwnedSessionTranscriptWrites(
565+{
566+ sessionFile,
567+canAdvanceSessionEntryCache: () => {
568+cacheAdvanceChecks += 1;
569+return true;
570+},
571+ publishSessionFileSnapshot,
572+withSessionWriteLock: async (run) => await run(),
573+},
574+async () => {
575+append(sessionManager, value);
576+},
577+);
578+} finally {
579+cleanup?.();
580+}
581+582+expect(
583+SessionManager.open(sessionFile, dir, dir)
584+.getEntries()
585+.filter((entry) => entry.type === "message")
586+.map((entry) => readMessageContent(entry)),
587+).toEqual(name === "tool_result_details" ? ["changed 1", "ok"] : ["changed 1"]);
588+expect(cacheAdvanceChecks, `${name}/${serializerCase.name}`).toBe(0);
589+expect(publishSessionFileSnapshot, `${name}/${serializerCase.name}`).not.toHaveBeenCalled();
590+}
454591}
455592});
456593此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。