Merge PR #8650: fix(debug): scope report bundle to current session subtree (@roboomp)

This commit is contained in:
can1357
2026-08-16 02:43:31 +02:00
3 changed files with 104 additions and 59 deletions
+3
View File
@@ -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);
});
});