Merge PR #8650: fix(debug): scope report bundle to current session subtree (@roboomp)
This commit is contained in:
@@ -57,6 +57,9 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed focused `ultrathink`, `orchestrate`, and `workflowz` shimmer frames repainting the full TUI every 70 ms, causing high CPU usage while composing prompts on WSL2 ([#8646](https://github.com/can1357/oh-my-pi/issues/8646)).
|
||||
### Fixed
|
||||
|
||||
- Fixed the `/debug` report bundle including unrelated historic sessions: subagent transcripts were collected by scanning the entire sessions directory, leaking other sessions' `.jsonl` files and bloating archives to tens of MB. The bundle now captures only the current session's own artifacts subtree ([#8648](https://github.com/can1357/oh-my-pi/issues/8648)).
|
||||
|
||||
## [17.3.4] - 2026-08-14
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* Creates a .tar.gz archive with session data, logs, system info, and optional profiling data.
|
||||
*/
|
||||
|
||||
import type { Dirent } from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import type { WorkProfile } from "@oh-my-pi/pi-natives";
|
||||
@@ -66,8 +68,8 @@ export interface DebugLogSource {
|
||||
*
|
||||
* Bundle contents:
|
||||
* - session.jsonl: Current session transcript
|
||||
* - artifacts/: Session artifacts directory
|
||||
* - subagents/: Subagent sessions + artifacts
|
||||
* - artifacts/: Current session's artifacts subtree (recursive), including any
|
||||
* subagent session transcripts nested under it
|
||||
* - logs.txt: Recent log entries
|
||||
* - system.json: OS, arch, CPU, memory, versions
|
||||
* - env.json: Sanitized environment variables
|
||||
@@ -130,14 +132,12 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
||||
// Session file might not exist yet
|
||||
}
|
||||
|
||||
// Artifacts directory (same path without .jsonl)
|
||||
// Artifacts subtree (same path without .jsonl). Recursing captures the
|
||||
// current session's nested subagent transcripts and their artifacts while
|
||||
// staying inside this session's own directory — unrelated co-located
|
||||
// sessions in the sessions root are never touched (#8648).
|
||||
const artifactsDir = options.sessionFile.slice(0, -6);
|
||||
await addDirectoryToArchive(data, files, artifactsDir, "artifacts");
|
||||
|
||||
// Look for subagent sessions in the same directory
|
||||
const sessionDir = path.dirname(options.sessionFile);
|
||||
const sessionBasename = path.basename(options.sessionFile, ".jsonl");
|
||||
await addSubagentSessions(data, files, sessionDir, sessionBasename);
|
||||
}
|
||||
|
||||
// CPU profile
|
||||
@@ -172,68 +172,34 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
||||
return { path: outputPath, files };
|
||||
}
|
||||
|
||||
/** Add all files from a directory to the archive */
|
||||
/** Recursively add every file under a directory to the archive. */
|
||||
async function addDirectoryToArchive(
|
||||
data: Record<string, string>,
|
||||
files: string[],
|
||||
dirPath: string,
|
||||
archivePrefix: string,
|
||||
): Promise<void> {
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const filePath = path.join(dirPath, entry.name);
|
||||
const archivePath = `${archivePrefix}/${entry.name}`;
|
||||
try {
|
||||
const content = await Bun.file(filePath).text();
|
||||
data[archivePath] = content;
|
||||
files.push(archivePath);
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
}
|
||||
}
|
||||
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
} catch {
|
||||
// Directory doesn't exist
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** Find and add subagent session files */
|
||||
async function addSubagentSessions(
|
||||
data: Record<string, string>,
|
||||
files: string[],
|
||||
sessionDir: string,
|
||||
parentBasename: string,
|
||||
): Promise<void> {
|
||||
// Subagent sessions are named with task IDs in the same directory
|
||||
// They follow the pattern: {timestamp}_{sessionId}.jsonl
|
||||
// We look for any sessions created after the parent session
|
||||
try {
|
||||
const entries = await fs.readdir(sessionDir, { withFileTypes: true });
|
||||
const sessionFiles = entries
|
||||
.filter(e => e.isFile() && e.name.endsWith(".jsonl") && e.name !== `${parentBasename}.jsonl`)
|
||||
.map(e => e.name);
|
||||
|
||||
// Limit to most recent 10 subagent sessions
|
||||
const sortedFiles = sessionFiles.sort().slice(-10);
|
||||
|
||||
for (const filename of sortedFiles) {
|
||||
const filePath = path.join(sessionDir, filename);
|
||||
const archivePath = `subagents/${filename}`;
|
||||
try {
|
||||
const content = await Bun.file(filePath).text();
|
||||
data[archivePath] = content;
|
||||
files.push(archivePath);
|
||||
|
||||
// Also add artifacts for this subagent session
|
||||
const artifactsDir = filePath.slice(0, -6);
|
||||
await addDirectoryToArchive(data, files, artifactsDir, `subagents/${filename.slice(0, -6)}`);
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dirPath, entry.name);
|
||||
const archivePath = `${archivePrefix}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
await addDirectoryToArchive(data, files, entryPath, archivePath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
try {
|
||||
data[archivePath] = await Bun.file(entryPath).text();
|
||||
files.push(archivePath);
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { createReportBundle } from "@oh-my-pi/pi-coding-agent/debug/report-bundle";
|
||||
import { getConfigRootDir, removeWithRetries, setAgentDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const originalXdgStateHome = process.env.XDG_STATE_HOME;
|
||||
const fallbackAgentDir = path.join(getConfigRootDir(), "agent");
|
||||
let cleanupRoot: string | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalXdgStateHome === undefined) {
|
||||
delete process.env.XDG_STATE_HOME;
|
||||
} else {
|
||||
process.env.XDG_STATE_HOME = originalXdgStateHome;
|
||||
}
|
||||
if (originalAgentDir) {
|
||||
setAgentDir(originalAgentDir);
|
||||
} else {
|
||||
setAgentDir(fallbackAgentDir);
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
}
|
||||
if (cleanupRoot) {
|
||||
await removeWithRetries(cleanupRoot);
|
||||
cleanupRoot = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
async function archiveMembers(archivePath: string): Promise<string[]> {
|
||||
const archive = new Bun.Archive(await Bun.file(archivePath).bytes());
|
||||
return [...(await archive.files()).keys()].sort();
|
||||
}
|
||||
|
||||
describe("report bundle sessions", () => {
|
||||
it("bundles only the current session's subtree, not unrelated co-located sessions", async () => {
|
||||
cleanupRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-report-sessions-"));
|
||||
const xdgStateHome = path.join(cleanupRoot, "state");
|
||||
await fs.mkdir(path.join(xdgStateHome, "omp"), { recursive: true });
|
||||
process.env.XDG_STATE_HOME = xdgStateHome;
|
||||
setAgentDir(fallbackAgentDir);
|
||||
|
||||
const sessionsDir = path.join(cleanupRoot, "sessions");
|
||||
await fs.mkdir(sessionsDir, { recursive: true });
|
||||
|
||||
// Current session and its artifacts subtree: a genuine subagent transcript
|
||||
// plus a nested sub-subagent transcript one level deeper.
|
||||
const sessionFile = path.join(sessionsDir, "2026-08-15T00-00-00_CURRENT.jsonl");
|
||||
await Bun.write(sessionFile, '{"type":"session","id":"CURRENT"}\n');
|
||||
const artifactsDir = sessionFile.slice(0, -6);
|
||||
await fs.mkdir(path.join(artifactsDir, "SubTask"), { recursive: true });
|
||||
await Bun.write(path.join(artifactsDir, "SubTask.jsonl"), '{"type":"session","id":"SubTask"}\n');
|
||||
await Bun.write(path.join(artifactsDir, "SubTask", "NestedTask.jsonl"), '{"type":"session","id":"NestedTask"}\n');
|
||||
|
||||
// Unrelated top-level sessions co-located in the sessions root.
|
||||
await Bun.write(
|
||||
path.join(sessionsDir, "2026-08-10T00-00-00_OTHERA.jsonl"),
|
||||
'{"type":"session","secret":"private-a"}\n',
|
||||
);
|
||||
await Bun.write(
|
||||
path.join(sessionsDir, "2026-08-12T00-00-00_OTHERB.jsonl"),
|
||||
'{"type":"session","secret":"private-b"}\n',
|
||||
);
|
||||
|
||||
const result = await createReportBundle({ sessionFile });
|
||||
const members = await archiveMembers(result.path);
|
||||
await fs.rm(result.path, { force: true });
|
||||
|
||||
// Genuine subtree is captured recursively.
|
||||
expect(members).toContain("artifacts/SubTask.jsonl");
|
||||
expect(members).toContain("artifacts/SubTask/NestedTask.jsonl");
|
||||
// Unrelated sessions never appear anywhere in the archive.
|
||||
expect(members.some(name => name.includes("OTHERA") || name.includes("OTHERB"))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user