

























@@ -20,7 +20,6 @@ vi.mock("../session-write-lock.js", () =>
2020),
2121);
222223-let rewriteTranscriptEntriesInSessionFile: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionFile;
2423let rewriteTranscriptEntriesInSessionManager: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionManager;
2524let rewriteTranscriptEntriesInRuntimeTranscript: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInRuntimeTranscript;
2625let onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate;
@@ -50,10 +49,6 @@ function createTextContent(text: string) {
5049return [{ type: "text", text }];
5150}
525153-function getMessageContent(message: AgentMessage): unknown {
54-return "content" in message ? message.content : undefined;
55-}
56-5752function createReadRewriteSession(options?: { tailAssistantText?: string }) {
5853// Read rewrite fixtures include a suffix assistant turn so branch rewrites
5954// must re-append downstream entries after replacing the tool result.
@@ -162,7 +157,6 @@ beforeAll(async () => {
162157({ installSessionToolResultGuard } = await import("../session-tool-result-guard.js"));
163158({
164159 rewriteTranscriptEntriesInRuntimeTranscript,
165- rewriteTranscriptEntriesInSessionFile,
166160 rewriteTranscriptEntriesInSessionManager,
167161} = await import("./transcript-rewrite.js"));
168162});
@@ -309,7 +303,7 @@ describe("rewriteTranscriptEntriesInSessionManager", () => {
309303});
310304});
311305312-describe("rewriteTranscriptEntriesInSessionFile", () => {
306+describe("rewriteTranscriptEntriesInRuntimeTranscript", () => {
313307it("does not create session metadata for missing runtime transcripts", async () => {
314308const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-runtime-"));
315309const storePath = path.join(dir, "sessions.json");
@@ -418,319 +412,4 @@ describe("rewriteTranscriptEntriesInSessionFile", () => {
418412}
419413});
420414421-it("aborts under the write lock when the active suffix contains an unexpected entry", async () => {
422-const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-guard-"));
423-const sessionManager = SessionManager.create(dir, dir);
424-const entryIds = appendSessionMessages(sessionManager, [
425-asAppendMessage({
426-role: "user",
427-content: "start",
428-timestamp: 1,
429-}),
430-asAppendMessage({
431-role: "assistant",
432-content: createTextContent("source reply media"),
433-timestamp: 2,
434-}),
435-asAppendMessage({
436-role: "assistant",
437-content: createTextContent("source reply text"),
438-timestamp: 3,
439-}),
440-asAppendMessage({
441-role: "user",
442-content: "concurrent append",
443-timestamp: 4,
444-}),
445-]);
446-const sessionFile = requireString(sessionManager.getSessionFile(), "persisted session file");
447-const mediaEntryId = entryIds[1];
448-const textEntryId = entryIds[2];
449-const listener = vi.fn();
450-const cleanup = onSessionTranscriptUpdate(listener);
451-452-try {
453-const result = await rewriteTranscriptEntriesInSessionFile({
454- sessionFile,
455-sessionKey: "agent:main:test",
456-request: {
457-allowedRewriteSuffixEntryIds: [mediaEntryId, textEntryId],
458-replacements: [
459-{
460-entryId: mediaEntryId,
461-message: asAppendMessage({
462-role: "assistant",
463-content: createTextContent("rewritten source reply media"),
464-timestamp: 2,
465-}) as AgentMessage,
466-},
467-],
468-},
469-});
470-471-expect(result).toMatchObject({
472-changed: false,
473-reason: "rewrite suffix guard failed",
474-});
475-expect(listener).not.toHaveBeenCalled();
476-477-const unchangedSession = SessionManager.open(sessionFile);
478-expect(getBranchMessages(unchangedSession).map(getMessageContent)).toEqual([
479-"start",
480-createTextContent("source reply media"),
481-createTextContent("source reply text"),
482-"concurrent append",
483-]);
484-} finally {
485-cleanup();
486-}
487-});
488-489-it("rewrites a guarded side branch and restores the active navigation state", async () => {
490-const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-side-"));
491-const sessionFile = path.join(dir, "session.jsonl");
492-await fs.writeFile(
493-sessionFile,
494-[
495-{
496-type: "session",
497-version: 3,
498-id: "session-side-rewrite",
499-timestamp: "2026-06-15T00:00:00.000Z",
500-cwd: dir,
501-},
502-{
503-type: "message",
504-id: "active-root",
505-parentId: null,
506-timestamp: "2026-06-15T00:00:01.000Z",
507-message: { role: "user", content: "active root", timestamp: 1 },
508-},
509-{
510-type: "message",
511-id: "side-mirror",
512-parentId: "active-root",
513-timestamp: "2026-06-15T00:00:02.000Z",
514-message: {
515-role: "assistant",
516-content: createTextContent("source reply before rewrite"),
517-timestamp: 2,
518-},
519-},
520-{
521-type: "leaf",
522-id: "active-leaf",
523-parentId: "side-mirror",
524-timestamp: "2026-06-15T00:00:03.000Z",
525-targetId: "active-root",
526-appendParentId: "side-mirror",
527-appendMode: "side",
528-},
529-]
530-.map((entry) => JSON.stringify(entry))
531-.join("\n") + "\n",
532-"utf-8",
533-);
534-535-const result = await rewriteTranscriptEntriesInSessionFile({
536- sessionFile,
537-sessionKey: "agent:main:test",
538-request: {
539-allowedRewriteSuffixEntryIds: ["side-mirror"],
540-replacements: [
541-{
542-entryId: "side-mirror",
543-message: asAppendMessage({
544-role: "assistant",
545-content: createTextContent("source reply after rewrite"),
546-timestamp: 2,
547-}) as AgentMessage,
548-},
549-],
550-},
551-});
552-553-expect(result).toMatchObject({ changed: true, rewrittenEntries: 1 });
554-const records = (await fs.readFile(sessionFile, "utf-8"))
555-.trim()
556-.split("\n")
557-.map(
558-(line) =>
559-JSON.parse(line) as {
560-type?: string;
561-id?: string;
562-parentId?: string | null;
563-targetId?: string | null;
564-appendParentId?: string | null;
565-appendMode?: "side";
566-message?: AgentMessage;
567-},
568-);
569-const rewrittenSideEntry = records.findLast(
570-(entry) =>
571-entry.type === "message" &&
572-JSON.stringify(entry.message).includes("source reply after rewrite"),
573-);
574-expect(rewrittenSideEntry).toMatchObject({ parentId: "active-root" });
575-expect(records.at(-1)).toMatchObject({
576-type: "leaf",
577-parentId: rewrittenSideEntry?.id,
578-targetId: "active-root",
579-appendParentId: "side-mirror",
580-appendMode: "side",
581-});
582-583-const reopened = SessionManager.open(sessionFile, dir, dir);
584-expect(getBranchMessages(reopened).map(getMessageContent)).toEqual(["active root"]);
585-const nextId = reopened.appendMessage(
586-asAppendMessage({ role: "user", content: "active continuation", timestamp: 3 }),
587-);
588-expect(reopened.getEntry(nextId)).toMatchObject({ parentId: "active-root" });
589-expect(reopened.getEntry(nextId)).not.toHaveProperty("appendMode");
590-});
591-592-it("rejects a rewrite batch split across active and side branches", async () => {
593-const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-mixed-"));
594-const sessionFile = path.join(dir, "session.jsonl");
595-const records = [
596-{
597-type: "session",
598-version: 3,
599-id: "session-mixed-rewrite",
600-timestamp: "2026-06-15T00:00:00.000Z",
601-cwd: dir,
602-},
603-{
604-type: "message",
605-id: "root",
606-parentId: null,
607-timestamp: "2026-06-15T00:00:01.000Z",
608-message: { role: "user", content: "root", timestamp: 1 },
609-},
610-{
611-type: "message",
612-id: "active-mirror",
613-parentId: "root",
614-timestamp: "2026-06-15T00:00:02.000Z",
615-message: { role: "assistant", content: createTextContent("active"), timestamp: 2 },
616-},
617-{
618-type: "message",
619-id: "side-mirror",
620-parentId: "root",
621-timestamp: "2026-06-15T00:00:03.000Z",
622-message: { role: "assistant", content: createTextContent("side"), timestamp: 3 },
623-},
624-{
625-type: "leaf",
626-id: "active-leaf",
627-parentId: "side-mirror",
628-timestamp: "2026-06-15T00:00:04.000Z",
629-targetId: "active-mirror",
630-},
631-];
632-const original = records.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
633-await fs.writeFile(sessionFile, original, "utf-8");
634-635-const result = await rewriteTranscriptEntriesInSessionFile({
636- sessionFile,
637-sessionKey: "agent:main:test",
638-request: {
639-allowedRewriteSuffixEntryIds: ["active-mirror", "side-mirror"],
640-replacements: [
641-{
642-entryId: "active-mirror",
643-message: asAppendMessage({
644-role: "assistant",
645-content: createTextContent("active rewritten"),
646-timestamp: 2,
647-}) as AgentMessage,
648-},
649-{
650-entryId: "side-mirror",
651-message: asAppendMessage({
652-role: "assistant",
653-content: createTextContent("side rewritten"),
654-timestamp: 3,
655-}) as AgentMessage,
656-},
657-],
658-},
659-});
660-661-expect(result).toMatchObject({
662-changed: false,
663-reason: "rewrite targets span multiple branches",
664-});
665-expect(await fs.readFile(sessionFile, "utf-8")).toBe(original);
666-});
667-668-it("emits transcript updates when the active branch changes without opening a manager", async () => {
669-const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-transcript-rewrite-"));
670-const sessionManager = SessionManager.create(dir, dir);
671-const entryIds = appendSessionMessages(sessionManager, [
672-asAppendMessage({
673-role: "user",
674-content: "run tool",
675-timestamp: 1,
676-}),
677-asAppendMessage({
678-role: "toolResult",
679-toolCallId: "call_1",
680-toolName: "exec",
681-content: createTextContent("before rewrite"),
682-isError: false,
683-timestamp: 2,
684-}),
685-asAppendMessage({
686-role: "assistant",
687-content: createTextContent("summarized"),
688-timestamp: 3,
689-}),
690-]);
691-const sessionFile = requireString(sessionManager.getSessionFile(), "persisted session file");
692-const toolResultEntryId = entryIds[1];
693-694-const openSpy = vi.spyOn(SessionManager, "open").mockImplementation(() => {
695-throw new Error("SessionManager.open should not be used for file rewrites");
696-});
697-const listener = vi.fn();
698-const cleanup = onSessionTranscriptUpdate(listener);
699-700-try {
701-const result = await rewriteTranscriptEntriesInSessionFile({
702- sessionFile,
703-sessionKey: "agent:main:test",
704-request: {
705-replacements: [
706-{
707-entryId: toolResultEntryId,
708-message: createToolResultReplacement("exec", "[file_ref:file_abc]", 2),
709-},
710-],
711-},
712-});
713-714-expect(result.changed).toBe(true);
715-expect(acquireSessionWriteLockMock).toHaveBeenCalledWith({
716- sessionFile,
717-staleMs: 1_800_000,
718-timeoutMs: 60_000,
719-maxHoldMs: 300_000,
720-});
721-expect(acquireSessionWriteLockReleaseMock).toHaveBeenCalledTimes(1);
722-expect(listener).toHaveBeenCalledWith({ sessionFile, sessionKey: "agent:main:test" });
723-724-openSpy.mockRestore();
725-const rewrittenSession = SessionManager.open(sessionFile);
726-const rewrittenToolResult = getBranchMessages(rewrittenSession)[1] as Extract<
727-AgentMessage,
728-{ role: "toolResult" }
729->;
730-expect(rewrittenToolResult.content).toEqual([{ type: "text", text: "[file_ref:file_abc]" }]);
731-} finally {
732-cleanup();
733-openSpy.mockRestore();
734-}
735-});
736415});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。