mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(server): delete ephemeral Claude session transcript on close
Claude Code ignores --no-session-persistence outside --print mode, so the SDK's persistSession=false is silently dropped in stream-json mode and the metadata/branch-name generator sessions kept showing up as resumable. Sweep the transcript on close when persistSession=false so internal generator runs no longer leak into `claude` session listings.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
@@ -701,6 +704,133 @@ describe("ClaudeAgentSession context window usage", () => {
|
||||
expect(persistedQueryFactory.mock.calls[0]?.[0].options.persistSession).toBe(true);
|
||||
});
|
||||
|
||||
test("deletes the persisted session jsonl on close when persistSession=false", async () => {
|
||||
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-persist-"));
|
||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpConfigDir;
|
||||
|
||||
try {
|
||||
const sessionId = "session-ephemeral";
|
||||
const cwd = "/tmp/paseo-test-claude";
|
||||
const sanitized = cwd.replace(/[\\/._:]/g, "-");
|
||||
const projectDir = path.join(tmpConfigDir, "projects", sanitized);
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
const sessionFile = path.join(projectDir, `${sessionId}.jsonl`);
|
||||
|
||||
const queryFactory = createQueryFactoryForTurns([
|
||||
[
|
||||
{
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
session_id: sessionId,
|
||||
permissionMode: "default",
|
||||
},
|
||||
{
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
duration_ms: 10,
|
||||
duration_api_ms: 8,
|
||||
is_error: false,
|
||||
num_turns: 1,
|
||||
result: "done",
|
||||
stop_reason: null,
|
||||
total_cost_usd: 0,
|
||||
usage: {},
|
||||
permission_denials: [],
|
||||
uuid: `${sessionId}-result`,
|
||||
session_id: sessionId,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({ provider: "claude", cwd }, undefined, {
|
||||
persistSession: false,
|
||||
});
|
||||
await session.run("turn");
|
||||
|
||||
// Simulate the claude binary writing a session transcript even though we
|
||||
// asked the SDK for ephemeral mode (the CLI ignores --no-session-persistence
|
||||
// outside --print, see issue context).
|
||||
await fs.writeFile(sessionFile, '{"type":"summary"}\n', "utf-8");
|
||||
|
||||
await session.close();
|
||||
|
||||
await expect(fs.access(sessionFile)).rejects.toThrow();
|
||||
} finally {
|
||||
if (previousConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
||||
}
|
||||
await fs.rm(tmpConfigDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves the persisted session jsonl on close when persistSession is undefined", async () => {
|
||||
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-persist-"));
|
||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpConfigDir;
|
||||
|
||||
try {
|
||||
const sessionId = "session-persistent";
|
||||
const cwd = "/tmp/paseo-test-claude";
|
||||
const sanitized = cwd.replace(/[\\/._:]/g, "-");
|
||||
const projectDir = path.join(tmpConfigDir, "projects", sanitized);
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
const sessionFile = path.join(projectDir, `${sessionId}.jsonl`);
|
||||
|
||||
const queryFactory = createQueryFactoryForTurns([
|
||||
[
|
||||
{
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
session_id: sessionId,
|
||||
permissionMode: "default",
|
||||
},
|
||||
{
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
duration_ms: 10,
|
||||
duration_api_ms: 8,
|
||||
is_error: false,
|
||||
num_turns: 1,
|
||||
result: "done",
|
||||
stop_reason: null,
|
||||
total_cost_usd: 0,
|
||||
usage: {},
|
||||
permission_denials: [],
|
||||
uuid: `${sessionId}-result`,
|
||||
session_id: sessionId,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({ provider: "claude", cwd });
|
||||
await session.run("turn");
|
||||
|
||||
await fs.writeFile(sessionFile, '{"type":"summary"}\n', "utf-8");
|
||||
|
||||
await session.close();
|
||||
|
||||
await expect(fs.access(sessionFile)).resolves.toBeUndefined();
|
||||
} finally {
|
||||
if (previousConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
||||
}
|
||||
await fs.rm(tmpConfigDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("convertUsage includes contextWindowMaxTokens and derives used tokens from result usage as initial fallback", async () => {
|
||||
const session = await createSessionForTest();
|
||||
|
||||
|
||||
@@ -1892,6 +1892,23 @@ class ClaudeAgentSession implements AgentSession {
|
||||
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
|
||||
this.query = null;
|
||||
this.input = null;
|
||||
if (this.persistSession === false && this.claudeSessionId) {
|
||||
// Claude Code currently ignores --no-session-persistence outside --print mode
|
||||
// (see `claude --help`), so the SDK's persistSession=false is silently dropped
|
||||
// in stream-json mode. Sweep the transcript ourselves so ephemeral runs
|
||||
// (metadata generator, branch-name generator) don't show up as resumable.
|
||||
const historyPath = this.resolveHistoryPath(this.claudeSessionId);
|
||||
if (historyPath) {
|
||||
try {
|
||||
await promises.rm(historyPath, { force: true });
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
{ err: error, historyPath, claudeSessionId: this.claudeSessionId },
|
||||
"Failed to delete ephemeral Claude session transcript",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.logger.trace(
|
||||
{ claudeSessionId: this.claudeSessionId, turnState: this.turnState },
|
||||
"Claude session close: completed",
|
||||
|
||||
Reference in New Issue
Block a user