




















@@ -9,6 +9,7 @@ import type { EmbeddedPiRunResult } from "../pi-embedded.js";
99import { persistCliTurnTranscript, runAgentAttempt } from "./attempt-execution.js";
10101111const runCliAgentMock = vi.hoisted(() => vi.fn());
12+const ORIGINAL_HOME = process.env.HOME;
12131314vi.mock("../cli-runner.js", () => ({
1415runCliAgent: runCliAgentMock,
@@ -75,11 +76,28 @@ describe("CLI attempt execution", () => {
7576});
76777778afterEach(async () => {
79+if (ORIGINAL_HOME === undefined) {
80+delete process.env.HOME;
81+} else {
82+process.env.HOME = ORIGINAL_HOME;
83+}
7884await fs.rm(tmpDir, { recursive: true, force: true });
7985});
80868187it("clears stale Claude CLI session IDs before retrying after session expiration", async () => {
8288const sessionKey = "agent:main:subagent:cli-expired";
89+const homeDir = path.join(tmpDir, "home");
90+const projectsDir = path.join(homeDir, ".claude", "projects", "demo-workspace");
91+process.env.HOME = homeDir;
92+await fs.mkdir(projectsDir, { recursive: true });
93+await fs.writeFile(
94+path.join(projectsDir, "stale-cli-session.jsonl"),
95+`${JSON.stringify({
96+ type: "assistant",
97+ message: { role: "assistant", content: [{ type: "text", text: "old reply" }] },
98+ })}\n`,
99+"utf-8",
100+);
83101const sessionEntry: SessionEntry = {
84102sessionId: "session-cli-123",
85103updatedAt: Date.now(),
@@ -143,6 +161,144 @@ describe("CLI attempt execution", () => {
143161expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined();
144162});
145163164+it("does not pass --resume when the stored Claude CLI transcript is missing", async () => {
165+const sessionKey = "agent:main:direct:claude-missing-transcript";
166+const homeDir = path.join(tmpDir, "home");
167+process.env.HOME = homeDir;
168+const sessionEntry: SessionEntry = {
169+sessionId: "openclaw-session-123",
170+updatedAt: Date.now(),
171+cliSessionBindings: {
172+"claude-cli": {
173+sessionId: "phantom-claude-session",
174+authProfileId: "anthropic:claude-cli",
175+},
176+},
177+cliSessionIds: { "claude-cli": "phantom-claude-session" },
178+claudeCliSessionId: "phantom-claude-session",
179+};
180+const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
181+await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
182+runCliAgentMock.mockResolvedValueOnce(makeCliResult("fresh cli response"));
183+184+await runAgentAttempt({
185+providerOverride: "claude-cli",
186+modelOverride: "opus",
187+cfg: {} as OpenClawConfig,
188+ sessionEntry,
189+sessionId: sessionEntry.sessionId,
190+ sessionKey,
191+sessionAgentId: "main",
192+sessionFile: path.join(tmpDir, "session.jsonl"),
193+workspaceDir: tmpDir,
194+body: "remember me",
195+isFallbackRetry: false,
196+resolvedThinkLevel: "medium",
197+timeoutMs: 1_000,
198+runId: "run-cli-missing-transcript",
199+opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
200+runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
201+spawnedBy: undefined,
202+messageChannel: undefined,
203+skillsSnapshot: undefined,
204+resolvedVerboseLevel: undefined,
205+agentDir: tmpDir,
206+onAgentEvent: vi.fn(),
207+authProfileProvider: "claude-cli",
208+ sessionStore,
209+ storePath,
210+sessionHasHistory: false,
211+});
212+213+expect(runCliAgentMock).toHaveBeenCalledTimes(1);
214+expect(runCliAgentMock.mock.calls[0]?.[0]?.cliSessionId).toBeUndefined();
215+expect(runCliAgentMock.mock.calls[0]?.[0]?.cliSessionBinding).toBeUndefined();
216+expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined();
217+expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
218+expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined();
219+220+const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
221+string,
222+SessionEntry
223+>;
224+expect(persisted[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined();
225+expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined();
226+expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined();
227+});
228+229+it("keeps Claude CLI resume when the stored transcript has assistant content", async () => {
230+const sessionKey = "agent:main:direct:claude-transcript-present";
231+const cliSessionId = "existing-claude-session";
232+const homeDir = path.join(tmpDir, "home");
233+const projectsDir = path.join(homeDir, ".claude", "projects", "demo-workspace");
234+process.env.HOME = homeDir;
235+await fs.mkdir(projectsDir, { recursive: true });
236+await fs.writeFile(
237+path.join(projectsDir, `${cliSessionId}.jsonl`),
238+`${JSON.stringify({
239+ type: "assistant",
240+ message: {
241+ role: "assistant",
242+ content: [{ type: "text", text: "previous reply" }],
243+ },
244+ })}\n`,
245+"utf-8",
246+);
247+const sessionEntry: SessionEntry = {
248+sessionId: "openclaw-session-456",
249+updatedAt: Date.now(),
250+cliSessionBindings: {
251+"claude-cli": {
252+sessionId: cliSessionId,
253+authProfileId: "anthropic:claude-cli",
254+},
255+},
256+cliSessionIds: { "claude-cli": cliSessionId },
257+claudeCliSessionId: cliSessionId,
258+};
259+const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
260+await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
261+runCliAgentMock.mockResolvedValueOnce(makeCliResult("resumed cli response"));
262+263+await runAgentAttempt({
264+providerOverride: "claude-cli",
265+modelOverride: "opus",
266+cfg: {} as OpenClawConfig,
267+ sessionEntry,
268+sessionId: sessionEntry.sessionId,
269+ sessionKey,
270+sessionAgentId: "main",
271+sessionFile: path.join(tmpDir, "session.jsonl"),
272+workspaceDir: tmpDir,
273+body: "continue",
274+isFallbackRetry: false,
275+resolvedThinkLevel: "medium",
276+timeoutMs: 1_000,
277+runId: "run-cli-transcript-present",
278+opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
279+runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
280+spawnedBy: undefined,
281+messageChannel: undefined,
282+skillsSnapshot: undefined,
283+resolvedVerboseLevel: undefined,
284+agentDir: tmpDir,
285+onAgentEvent: vi.fn(),
286+authProfileProvider: "claude-cli",
287+ sessionStore,
288+ storePath,
289+sessionHasHistory: false,
290+});
291+292+expect(runCliAgentMock).toHaveBeenCalledTimes(1);
293+expect(runCliAgentMock.mock.calls[0]?.[0]?.cliSessionId).toBe(cliSessionId);
294+expect(runCliAgentMock.mock.calls[0]?.[0]?.cliSessionBinding).toEqual({
295+sessionId: cliSessionId,
296+authProfileId: "anthropic:claude-cli",
297+});
298+expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBe(cliSessionId);
299+expect(sessionStore[sessionKey]?.claudeCliSessionId).toBe(cliSessionId);
300+});
301+146302it("persists CLI replies into the session transcript", async () => {
147303const sessionKey = "agent:main:subagent:cli-transcript";
148304const sessionEntry: SessionEntry = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。