fix: workspace-scoped session imports across Claude Code, OpenCode, Pi, and OMP (#2265)
* fix(claude): scope import discovery to workspace * fix(providers): preserve workspace session discovery
This commit is contained in:
@@ -242,7 +242,7 @@ test("listImportableProviderSessions filters, sorts, limits, and projects import
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(listImportableSessions).toHaveBeenCalledWith({
|
expect(listImportableSessions).toHaveBeenCalledWith({
|
||||||
limit: 2,
|
limit: 4,
|
||||||
providerFilter: new Set(["codex"]),
|
providerFilter: new Set(["codex"]),
|
||||||
cwd,
|
cwd,
|
||||||
});
|
});
|
||||||
@@ -273,6 +273,50 @@ test("listImportableProviderSessions filters, sorts, limits, and projects import
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("listImportableProviderSessions looks past already-imported rows to fill the requested limit", async () => {
|
||||||
|
const cwd = "/tmp/project";
|
||||||
|
const imported = makeImportableSession({
|
||||||
|
provider: "claude",
|
||||||
|
sessionId: "already-imported",
|
||||||
|
cwd,
|
||||||
|
lastActivityAt: "2026-04-30T12:02:00.000Z",
|
||||||
|
});
|
||||||
|
const available = makeImportableSession({
|
||||||
|
provider: "claude",
|
||||||
|
sessionId: "available",
|
||||||
|
cwd,
|
||||||
|
lastActivityAt: "2026-04-30T12:01:00.000Z",
|
||||||
|
});
|
||||||
|
const listImportableSessions = vi.fn(async (options?: { limit?: number }) =>
|
||||||
|
[imported, available].slice(0, options?.limit),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await listImportableProviderSessions({
|
||||||
|
request: makeRequest({ cwd, providers: ["claude"], limit: 1 }),
|
||||||
|
agentManager: {
|
||||||
|
listAgents: () => [],
|
||||||
|
listImportableSessions,
|
||||||
|
},
|
||||||
|
agentStorage: {
|
||||||
|
list: async () => [
|
||||||
|
{
|
||||||
|
provider: "claude",
|
||||||
|
persistence: { provider: "claude", sessionId: "already-imported" },
|
||||||
|
} as StoredAgentRecord,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
providerSnapshotManager: { getProviderLabel: () => "Claude Code" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listImportableSessions).toHaveBeenCalledWith({
|
||||||
|
limit: 2,
|
||||||
|
providerFilter: new Set(["claude"]),
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
|
expect(result.entries.map((entry) => entry.providerHandleId)).toEqual(["available"]);
|
||||||
|
expect(result.filteredAlreadyImportedCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("listImportableProviderSessions includes a provider session after its Paseo agent is archived", async () => {
|
test("listImportableProviderSessions includes a provider session after its Paseo agent is archived", async () => {
|
||||||
const cwd = "/tmp/project";
|
const cwd = "/tmp/project";
|
||||||
const archivedSession = makeImportableSession({
|
const archivedSession = makeImportableSession({
|
||||||
|
|||||||
@@ -121,10 +121,15 @@ export async function listImportableProviderSessions(
|
|||||||
const limit = request.limit ?? 20;
|
const limit = request.limit ?? 20;
|
||||||
const sinceTimestamp = parseRecentProviderSessionsSince(request.since);
|
const sinceTimestamp = parseRecentProviderSessionsSince(request.since);
|
||||||
const providerFilter = request.providers ? new Set(request.providers) : undefined;
|
const providerFilter = request.providers ? new Set(request.providers) : undefined;
|
||||||
const importedHandles = await collectImportedProviderSessionHandles(agentManager, agentStorage);
|
const importedSessions = await collectImportedProviderSessions(
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
providerFilter,
|
||||||
|
);
|
||||||
|
const importedHandles = importedSessions.handles;
|
||||||
|
|
||||||
const sessions = await agentManager.listImportableSessions({
|
const sessions = await agentManager.listImportableSessions({
|
||||||
limit,
|
limit: limit + importedSessions.count,
|
||||||
providerFilter,
|
providerFilter,
|
||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
});
|
});
|
||||||
@@ -328,29 +333,40 @@ function parseRecentProviderSessionsSince(since: string | undefined): number | n
|
|||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectImportedProviderSessionHandles(
|
async function collectImportedProviderSessions(
|
||||||
agentManager: Pick<AgentManager, "listAgents">,
|
agentManager: Pick<AgentManager, "listAgents">,
|
||||||
agentStorage: Pick<AgentStorage, "list">,
|
agentStorage: Pick<AgentStorage, "list">,
|
||||||
): Promise<Set<string>> {
|
providerFilter: Set<string> | undefined,
|
||||||
|
): Promise<{ handles: Set<string>; count: number }> {
|
||||||
const handles = new Set<string>();
|
const handles = new Set<string>();
|
||||||
|
const sessions = new Set<string>();
|
||||||
const records = await agentStorage.list();
|
const records = await agentStorage.list();
|
||||||
const storedRecordsById = new Map(records.map((record) => [record.id, record]));
|
const storedRecordsById = new Map(records.map((record) => [record.id, record]));
|
||||||
|
|
||||||
|
const collect = (
|
||||||
|
provider: AgentProvider | StoredAgentRecord["provider"] | string,
|
||||||
|
persistence: AgentPersistenceHandle | null | undefined,
|
||||||
|
) => {
|
||||||
|
if (!persistence || (providerFilter && !providerFilter.has(provider))) return;
|
||||||
|
sessions.add(toProviderSessionHandleKey(provider, persistence.sessionId));
|
||||||
|
collectProviderSessionHandleKeys(handles, provider, persistence);
|
||||||
|
};
|
||||||
|
|
||||||
for (const agent of agentManager.listAgents()) {
|
for (const agent of agentManager.listAgents()) {
|
||||||
if (storedRecordsById.get(agent.id)?.archivedAt) {
|
if (storedRecordsById.get(agent.id)?.archivedAt) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
collectProviderSessionHandleKeys(handles, agent.provider, agent.persistence);
|
collect(agent.provider, agent.persistence);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
if (record.archivedAt) {
|
if (record.archivedAt) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
collectProviderSessionHandleKeys(handles, record.provider, record.persistence);
|
collect(record.provider, record.persistence);
|
||||||
}
|
}
|
||||||
|
|
||||||
return handles;
|
return { handles, count: sessions.size };
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProviderSessionHandleKey(provider: string, providerHandleId: string): string {
|
function toProviderSessionHandleKey(provider: string, providerHandleId: string): string {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
normalizeClaudeAskUserQuestionUpdatedInput,
|
normalizeClaudeAskUserQuestionUpdatedInput,
|
||||||
toClaudeSdkMcpConfig,
|
toClaudeSdkMcpConfig,
|
||||||
} from "./agent.js";
|
} from "./agent.js";
|
||||||
|
import { claudeProjectDirSync } from "./project-dir.js";
|
||||||
import { streamSession } from "../test-utils/session-stream-adapter.js";
|
import { streamSession } from "../test-utils/session-stream-adapter.js";
|
||||||
import type { AgentSession, AgentTimelineItem, AgentStreamEvent } from "../../agent-sdk-types.js";
|
import type { AgentSession, AgentTimelineItem, AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||||
|
|
||||||
@@ -1081,6 +1082,75 @@ describe("normalizeClaudeAskUserQuestionUpdatedInput", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("ClaudeAgentClient.listImportableSessions", () => {
|
describe("ClaudeAgentClient.listImportableSessions", () => {
|
||||||
|
test("scopes candidates to the requested cwd before applying the limit", async () => {
|
||||||
|
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-import-"));
|
||||||
|
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = tmpConfigDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestedCwd = path.join(tmpConfigDir, "requested-project");
|
||||||
|
const busyCwd = path.join(tmpConfigDir, "busy-project");
|
||||||
|
await fs.mkdir(requestedCwd, { recursive: true });
|
||||||
|
await fs.mkdir(busyCwd, { recursive: true });
|
||||||
|
const requestedProjectDir = claudeProjectDirSync(requestedCwd, { configDir: tmpConfigDir });
|
||||||
|
const busyProjectDir = claudeProjectDirSync(busyCwd, { configDir: tmpConfigDir });
|
||||||
|
await fs.mkdir(requestedProjectDir, { recursive: true });
|
||||||
|
await fs.mkdir(busyProjectDir, { recursive: true });
|
||||||
|
|
||||||
|
const writeSession = async (
|
||||||
|
projectDir: string,
|
||||||
|
sessionId: string,
|
||||||
|
cwd: string,
|
||||||
|
day: number,
|
||||||
|
) => {
|
||||||
|
const file = path.join(projectDir, `${sessionId}.jsonl`);
|
||||||
|
await fs.writeFile(
|
||||||
|
file,
|
||||||
|
`${JSON.stringify({
|
||||||
|
isSidechain: false,
|
||||||
|
type: "user",
|
||||||
|
message: { role: "user", content: `Prompt for ${sessionId}` },
|
||||||
|
cwd,
|
||||||
|
sessionId,
|
||||||
|
})}\n`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
const timestamp = new Date(`2026-06-${String(day).padStart(2, "0")}T12:00:00.000Z`);
|
||||||
|
await fs.utimes(file, timestamp, timestamp);
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeSession(requestedProjectDir, "requested-session", requestedCwd, 1);
|
||||||
|
await writeSession(busyProjectDir, "newer-session-1", busyCwd, 2);
|
||||||
|
await writeSession(busyProjectDir, "newer-session-2", busyCwd, 3);
|
||||||
|
await writeSession(busyProjectDir, "newer-session-3", busyCwd, 4);
|
||||||
|
|
||||||
|
const client = new ClaudeAgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
resolveBinary: async () => "/test/claude/bin",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(client.listImportableSessions({ limit: 1, cwd: requestedCwd })).resolves.toEqual(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
providerHandleId: "requested-session",
|
||||||
|
cwd: requestedCwd,
|
||||||
|
title: "Prompt for requested-session",
|
||||||
|
firstPromptPreview: "Prompt for requested-session",
|
||||||
|
lastPromptPreview: "Prompt for requested-session",
|
||||||
|
lastActivityAt: new Date("2026-06-01T12:00:00.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} 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("shows Claude slash command prompts without transcript tags", async () => {
|
test("shows Claude slash command prompts without transcript tags", async () => {
|
||||||
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-import-"));
|
const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-import-"));
|
||||||
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||||
@@ -1235,7 +1305,7 @@ describe("ClaudeAgentSession context window usage", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
for await (const _prompt of prompt) {
|
for await (const _ of prompt) {
|
||||||
const turnMessages = turns[turnIndex] ?? [];
|
const turnMessages = turns[turnIndex] ?? [];
|
||||||
turnIndex += 1;
|
turnIndex += 1;
|
||||||
for (const message of turnMessages) {
|
for (const message of turnMessages) {
|
||||||
|
|||||||
@@ -1535,12 +1535,16 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
options?: ListImportableSessionsOptions,
|
options?: ListImportableSessionsOptions,
|
||||||
): Promise<ImportableProviderSession[]> {
|
): Promise<ImportableProviderSession[]> {
|
||||||
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
||||||
const projectsRoot = path.join(configDir, "projects");
|
const sessionsRoot = options?.cwd
|
||||||
if (!(await pathExists(projectsRoot))) {
|
? claudeProjectDirSync(options.cwd, { configDir })
|
||||||
|
: path.join(configDir, "projects");
|
||||||
|
if (!(await pathExists(sessionsRoot))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const limit = options?.limit ?? 20;
|
const limit = options?.limit ?? 20;
|
||||||
const candidates = await collectRecentClaudeSessions(projectsRoot, limit * 3);
|
const candidates = await collectRecentClaudeSessions(sessionsRoot, limit * 3, {
|
||||||
|
rootIsProjectDir: Boolean(options?.cwd),
|
||||||
|
});
|
||||||
const parsed = await Promise.all(
|
const parsed = await Promise.all(
|
||||||
candidates.map((candidate) => parseClaudeSessionDescriptor(candidate.path, candidate.mtime)),
|
candidates.map((candidate) => parseClaudeSessionDescriptor(candidate.path, candidate.mtime)),
|
||||||
);
|
);
|
||||||
@@ -5470,29 +5474,33 @@ async function pathExists(target: string): Promise<boolean> {
|
|||||||
async function collectRecentClaudeSessions(
|
async function collectRecentClaudeSessions(
|
||||||
root: string,
|
root: string,
|
||||||
limit: number,
|
limit: number,
|
||||||
|
options?: { rootIsProjectDir?: boolean },
|
||||||
): Promise<ClaudeSessionCandidate[]> {
|
): Promise<ClaudeSessionCandidate[]> {
|
||||||
let projectDirs: string[];
|
let rootEntries: string[];
|
||||||
try {
|
try {
|
||||||
projectDirs = await fsPromises.readdir(root);
|
rootEntries = await fsPromises.readdir(root);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const projectFileLists = await Promise.all(
|
const fileEntries = options?.rootIsProjectDir
|
||||||
projectDirs.map(async (dirName) => {
|
? rootEntries.filter((file) => file.endsWith(".jsonl")).map((file) => path.join(root, file))
|
||||||
const projectPath = path.join(root, dirName);
|
: (
|
||||||
try {
|
await Promise.all(
|
||||||
const stats = await fsPromises.stat(projectPath);
|
rootEntries.map(async (dirName) => {
|
||||||
if (!stats.isDirectory()) return { projectPath, files: [] as string[] };
|
const projectPath = path.join(root, dirName);
|
||||||
const files = await fsPromises.readdir(projectPath);
|
try {
|
||||||
return { projectPath, files };
|
const stats = await fsPromises.stat(projectPath);
|
||||||
} catch {
|
if (!stats.isDirectory()) return [] as string[];
|
||||||
return { projectPath, files: [] as string[] };
|
const files = await fsPromises.readdir(projectPath);
|
||||||
}
|
return files
|
||||||
}),
|
.filter((file) => file.endsWith(".jsonl"))
|
||||||
);
|
.map((file) => path.join(projectPath, file));
|
||||||
const fileEntries = projectFileLists.flatMap(({ projectPath, files }) =>
|
} catch {
|
||||||
files.filter((f) => f.endsWith(".jsonl")).map((f) => path.join(projectPath, f)),
|
return [] as string[];
|
||||||
);
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
const statResults = await Promise.all(
|
const statResults = await Promise.all(
|
||||||
fileEntries.map(async (fullPath) => {
|
fileEntries.map(async (fullPath) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,6 +13,42 @@ async function writeSession(root: string, relativePath: string, lines: unknown[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("OMP session descriptor", () => {
|
describe("OMP session descriptor", () => {
|
||||||
|
test("cwd filtering continues past the global candidate overscan", async () => {
|
||||||
|
const root = await mkdtemp(path.join(tmpdir(), "paseo-omp-session-cwd-limit-"));
|
||||||
|
const sessionsDir = path.join(root, "sessions");
|
||||||
|
const requestedCwd = path.join(root, "requested");
|
||||||
|
const otherCwd = path.join(root, "other");
|
||||||
|
const requestedFile = await writeSession(root, "requested/requested.jsonl", [
|
||||||
|
{
|
||||||
|
type: "session",
|
||||||
|
id: "requested-session",
|
||||||
|
timestamp: "2026-06-01T00:00:00.000Z",
|
||||||
|
cwd: requestedCwd,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await utimes(requestedFile, new Date("2026-06-01"), new Date("2026-06-01"));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: 400 }, async (_, index) => {
|
||||||
|
const file = await writeSession(root, `other/${index}.jsonl`, [
|
||||||
|
{
|
||||||
|
type: "session",
|
||||||
|
id: `other-${index}`,
|
||||||
|
timestamp: "2026-06-02T00:00:00.000Z",
|
||||||
|
cwd: otherCwd,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await utimes(file, new Date("2026-06-02"), new Date("2026-06-02"));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
listOmpImportableSessions({ sessionDir: sessionsDir, cwd: requestedCwd, limit: 1 }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
expect.objectContaining({ providerHandleId: requestedFile, cwd: requestedCwd }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("reads title-first sessions and OMP combined model identifiers", async () => {
|
test("reads title-first sessions and OMP combined model identifiers", async () => {
|
||||||
const root = await mkdtemp(path.join(tmpdir(), "paseo-omp-session-title-first-"));
|
const root = await mkdtemp(path.join(tmpdir(), "paseo-omp-session-title-first-"));
|
||||||
const cwd = path.join(root, "repo");
|
const cwd = path.join(root, "repo");
|
||||||
|
|||||||
@@ -80,9 +80,10 @@ export async function listOmpImportableSessions(
|
|||||||
const limit = options.limit ?? 20;
|
const limit = options.limit ?? 20;
|
||||||
const ranked = await rankSessionFilesByMtime(files);
|
const ranked = await rankSessionFilesByMtime(files);
|
||||||
const candidateLimit = Math.max(limit * IMPORT_CANDIDATE_OVERSCAN, IMPORT_CANDIDATE_MIN);
|
const candidateLimit = Math.max(limit * IMPORT_CANDIDATE_OVERSCAN, IMPORT_CANDIDATE_MIN);
|
||||||
|
const candidates = matchesCwd ? ranked : ranked.slice(0, candidateLimit);
|
||||||
const sessions: ImportableProviderSession[] = [];
|
const sessions: ImportableProviderSession[] = [];
|
||||||
|
|
||||||
for (const entry of ranked.slice(0, candidateLimit)) {
|
for (const entry of candidates) {
|
||||||
const session = await readOmpImportableSession(entry.file);
|
const session = await readOmpImportableSession(entry.file);
|
||||||
if (!session) continue;
|
if (!session) continue;
|
||||||
if (matchesCwd && !matchesCwd(session.cwd)) continue;
|
if (matchesCwd && !matchesCwd(session.cwd)) continue;
|
||||||
|
|||||||
@@ -2175,7 +2175,7 @@ describe("OpenCode persisted sessions", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("listImportableSessions returns rows without hydrating session messages", async () => {
|
test("listImportableSessions scopes upstream rows without hydrating session messages", async () => {
|
||||||
const runtime = new TestOpenCodeHarness();
|
const runtime = new TestOpenCodeHarness();
|
||||||
const openCodeClient = new TestOpenCodeClient();
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
const cwd = "/workspace/repo";
|
const cwd = "/workspace/repo";
|
||||||
@@ -2290,7 +2290,7 @@ describe("OpenCode persisted sessions", () => {
|
|||||||
expect(sessions[0]?.lastActivityAt.toISOString()).toBe("1970-01-01T00:00:03.000Z");
|
expect(sessions[0]?.lastActivityAt.toISOString()).toBe("1970-01-01T00:00:03.000Z");
|
||||||
expect(runtime.clientCreations).toEqual([{ baseUrl: runtime.server.url, directory: cwd }]);
|
expect(runtime.clientCreations).toEqual([{ baseUrl: runtime.server.url, directory: cwd }]);
|
||||||
expect(openCodeClient.calls.experimentalSessionList).toEqual([
|
expect(openCodeClient.calls.experimentalSessionList).toEqual([
|
||||||
{ archived: true, roots: true, limit: 200 },
|
{ archived: true, roots: true, limit: 200, directory: cwd },
|
||||||
]);
|
]);
|
||||||
expect(openCodeClient.calls.sessionMessages).toEqual([]);
|
expect(openCodeClient.calls.sessionMessages).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -2411,7 +2411,7 @@ describe("OpenCode persisted sessions", () => {
|
|||||||
title: "Windows session",
|
title: "Windows session",
|
||||||
});
|
});
|
||||||
expect(openCodeClient.calls.experimentalSessionList).toEqual([
|
expect(openCodeClient.calls.experimentalSessionList).toEqual([
|
||||||
{ archived: true, roots: true, limit: 200 },
|
{ archived: true, roots: true, limit: 200, directory: requestedCwd },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -947,6 +947,7 @@ async function collectOpenCodeImportableSessionsFromSdk(
|
|||||||
archived: true,
|
archived: true,
|
||||||
roots: true,
|
roots: true,
|
||||||
limit: sessionListLimit,
|
limit: sessionListLimit,
|
||||||
|
...(options?.cwd ? { directory: options.cwd } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
import { mkdtemp, mkdir, utimes, writeFile } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
@@ -13,6 +13,52 @@ async function writeSession(root: string, lines: unknown[]): Promise<string> {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("Pi cwd filtering continues past the global candidate overscan", async () => {
|
||||||
|
const root = await mkdtemp(path.join(tmpdir(), "paseo-pi-session-cwd-limit-"));
|
||||||
|
const sessionsDir = path.join(root, "sessions");
|
||||||
|
const requestedCwd = path.join(root, "requested");
|
||||||
|
const otherCwd = path.join(root, "other");
|
||||||
|
const requestedFile = path.join(sessionsDir, "requested", "requested.jsonl");
|
||||||
|
await mkdir(path.dirname(requestedFile), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
requestedFile,
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
version: 3,
|
||||||
|
id: "requested-session",
|
||||||
|
timestamp: "2026-06-01T00:00:00.000Z",
|
||||||
|
cwd: requestedCwd,
|
||||||
|
})}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await utimes(requestedFile, new Date("2026-06-01"), new Date("2026-06-01"));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: 400 }, async (_, index) => {
|
||||||
|
const file = path.join(sessionsDir, "other", `${index}.jsonl`);
|
||||||
|
await mkdir(path.dirname(file), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
file,
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
version: 3,
|
||||||
|
id: `other-${index}`,
|
||||||
|
timestamp: "2026-06-02T00:00:00.000Z",
|
||||||
|
cwd: otherCwd,
|
||||||
|
})}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await utimes(file, new Date("2026-06-02"), new Date("2026-06-02"));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
listPiImportableSessions({ sessionDir: sessionsDir, cwd: requestedCwd, limit: 1 }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
expect.objectContaining({ providerHandleId: requestedFile, cwd: requestedCwd }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("Pi import config preserves the latest recorded model and thinking level", async () => {
|
test("Pi import config preserves the latest recorded model and thinking level", async () => {
|
||||||
const root = await mkdtemp(path.join(tmpdir(), "paseo-pi-session-model-"));
|
const root = await mkdtemp(path.join(tmpdir(), "paseo-pi-session-model-"));
|
||||||
const cwd = path.join(root, "repo");
|
const cwd = path.join(root, "repo");
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ export async function listPiImportableSessions(
|
|||||||
const limit = options.limit ?? 20;
|
const limit = options.limit ?? 20;
|
||||||
const ranked = await rankSessionFilesByMtime(files);
|
const ranked = await rankSessionFilesByMtime(files);
|
||||||
const candidateLimit = Math.max(limit * IMPORT_CANDIDATE_OVERSCAN, IMPORT_CANDIDATE_MIN);
|
const candidateLimit = Math.max(limit * IMPORT_CANDIDATE_OVERSCAN, IMPORT_CANDIDATE_MIN);
|
||||||
|
const candidates = matchesCwd ? ranked : ranked.slice(0, candidateLimit);
|
||||||
const sessions: ImportableProviderSession[] = [];
|
const sessions: ImportableProviderSession[] = [];
|
||||||
|
|
||||||
for (const entry of ranked.slice(0, candidateLimit)) {
|
for (const entry of candidates) {
|
||||||
const session = await readPiImportableSession(entry.file);
|
const session = await readPiImportableSession(entry.file);
|
||||||
if (!session) continue;
|
if (!session) continue;
|
||||||
if (matchesCwd && !matchesCwd(session.cwd)) continue;
|
if (matchesCwd && !matchesCwd(session.cwd)) continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user