























@@ -3,7 +3,10 @@ import os from "node:os";
33import path from "node:path";
44import { afterEach, describe, expect, it } from "vitest";
55import type { SessionEntry } from "../../config/sessions/types.js";
6-import { resolveParentForkTokenCountRuntime } from "./session-fork.runtime.js";
6+import {
7+forkSessionFromParentRuntime,
8+resolveParentForkTokenCountRuntime,
9+} from "./session-fork.runtime.js";
710811const roots: string[] = [];
912@@ -71,3 +74,136 @@ describe("resolveParentForkTokenCountRuntime", () => {
7174expect(tokens).toBeGreaterThan(100_000);
7275});
7376});
77+78+describe("forkSessionFromParentRuntime", () => {
79+it("forks the active branch without synchronously opening the session manager", async () => {
80+const root = await makeRoot("openclaw-parent-fork-");
81+const sessionsDir = path.join(root, "sessions");
82+await fs.mkdir(sessionsDir);
83+const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
84+const cwd = path.join(root, "workspace");
85+await fs.mkdir(cwd);
86+const parentSessionId = "parent-session";
87+const lines = [
88+{
89+type: "session",
90+version: 3,
91+id: parentSessionId,
92+timestamp: "2026-05-01T00:00:00.000Z",
93+ cwd,
94+},
95+{
96+type: "message",
97+id: "user-1",
98+parentId: null,
99+timestamp: "2026-05-01T00:00:01.000Z",
100+message: { role: "user", content: "hello" },
101+},
102+{
103+type: "message",
104+id: "assistant-1",
105+parentId: "user-1",
106+timestamp: "2026-05-01T00:00:02.000Z",
107+message: {
108+role: "assistant",
109+content: [{ type: "text", text: "hi" }],
110+api: "openai-responses",
111+provider: "openai",
112+model: "gpt-5.4",
113+stopReason: "stop",
114+timestamp: 2,
115+},
116+},
117+{
118+type: "label",
119+id: "label-1",
120+parentId: "assistant-1",
121+timestamp: "2026-05-01T00:00:03.000Z",
122+targetId: "user-1",
123+label: "start",
124+},
125+];
126+await fs.writeFile(
127+parentSessionFile,
128+`${lines.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
129+"utf-8",
130+);
131+132+const fork = await forkSessionFromParentRuntime({
133+parentEntry: {
134+sessionId: parentSessionId,
135+sessionFile: parentSessionFile,
136+updatedAt: Date.now(),
137+},
138+agentId: "main",
139+ sessionsDir,
140+});
141+142+expect(fork).not.toBeNull();
143+expect(fork?.sessionFile).toContain(sessionsDir);
144+expect(fork?.sessionId).not.toBe(parentSessionId);
145+const raw = await fs.readFile(fork?.sessionFile ?? "", "utf-8");
146+const forkedEntries = raw
147+.trim()
148+.split(/\r?\n/u)
149+.map((line) => JSON.parse(line) as Record<string, unknown>);
150+const resolvedParentSessionFile = await fs.realpath(parentSessionFile);
151+expect(forkedEntries[0]).toMatchObject({
152+type: "session",
153+id: fork?.sessionId,
154+ cwd,
155+parentSession: resolvedParentSessionFile,
156+});
157+expect(forkedEntries.map((entry) => entry.type)).toEqual([
158+"session",
159+"message",
160+"message",
161+"label",
162+]);
163+expect(forkedEntries.at(-1)).toMatchObject({
164+type: "label",
165+targetId: "user-1",
166+label: "start",
167+});
168+});
169+170+it("creates a header-only child when the parent has no entries", async () => {
171+const root = await makeRoot("openclaw-parent-fork-empty-");
172+const sessionsDir = path.join(root, "sessions");
173+await fs.mkdir(sessionsDir);
174+const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
175+const parentSessionId = "parent-empty";
176+await fs.writeFile(
177+parentSessionFile,
178+`${JSON.stringify({
179+ type: "session",
180+ version: 3,
181+ id: parentSessionId,
182+ timestamp: "2026-05-01T00:00:00.000Z",
183+ cwd: root,
184+ })}\n`,
185+"utf-8",
186+);
187+188+const fork = await forkSessionFromParentRuntime({
189+parentEntry: {
190+sessionId: parentSessionId,
191+sessionFile: parentSessionFile,
192+updatedAt: Date.now(),
193+},
194+agentId: "main",
195+ sessionsDir,
196+});
197+198+expect(fork).not.toBeNull();
199+const raw = await fs.readFile(fork?.sessionFile ?? "", "utf-8");
200+const lines = raw.trim().split(/\r?\n/u);
201+expect(lines).toHaveLength(1);
202+const resolvedParentSessionFile = await fs.realpath(parentSessionFile);
203+expect(JSON.parse(lines[0] ?? "{}")).toMatchObject({
204+type: "session",
205+id: fork?.sessionId,
206+parentSession: resolvedParentSessionFile,
207+});
208+});
209+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。