Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/agent/subagent/log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,54 @@ describe('stream-open-window concurrent writes', () => {
});
});

// ---------------------------------------------------------------------------
// mkdirSync memoization — multiple writers with the same sessionLabel
// ---------------------------------------------------------------------------

describe('mkdirSync memoization', () => {
it('two writers sharing a sessionLabel both produce correct logs', async () => {
const session = 'test-memo-session';

const w1 = new SubagentLogWriter(session, 'sub-memo-1');
const w2 = new SubagentLogWriter(session, 'sub-memo-2');

w1.write(makeChunkEvent('from-w1'));
w2.write(makeChunkEvent('from-w2'));
await Promise.all([w1.close(), w2.close()]);

// Both log files must be readable — verifies the memoised path does not
// suppress directory creation for the first writer or break the second.
const events1: OutputEvent[] = [];
for await (const e of SubagentLogReader.readEvents(session, 'sub-memo-1')) {
events1.push(e);
}
const events2: OutputEvent[] = [];
for await (const e of SubagentLogReader.readEvents(session, 'sub-memo-2')) {
events2.push(e);
}
expect(events1).toHaveLength(1);
expect(events2).toHaveLength(1);
});

it('N writers sharing a sessionLabel all produce correct logs (stress)', async () => {
const session = 'test-memo-stress';
const N = 8;
const writers = Array.from({ length: N }, (_, i) =>
new SubagentLogWriter(session, `sub-stress-${i}`),
);
for (const [i, w] of writers.entries()) w.write(makeChunkEvent(`msg-${i}`));
await Promise.all(writers.map(w => w.close()));

for (let i = 0; i < N; i++) {
const events: OutputEvent[] = [];
for await (const e of SubagentLogReader.readEvents(session, `sub-stress-${i}`)) {
events.push(e);
}
expect(events).toHaveLength(1);
}
});
});

// ---------------------------------------------------------------------------
// MAX_LOG_BYTES cap
// ---------------------------------------------------------------------------
Expand Down
15 changes: 11 additions & 4 deletions src/agent/subagent/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import type { OutputEvent } from '../types/session-types.js';
/** Maximum bytes per subagent log file (1 MB). Writes beyond this are dropped. */
const MAX_LOG_BYTES = 1_048_576;

/** Session-log directories already created this process — avoids redundant mkdirSync syscalls. */
const createdDirs = new Set<string>();

// ---------------------------------------------------------------------------
// Writer
// ---------------------------------------------------------------------------
Expand All @@ -53,10 +56,14 @@ export class SubagentLogWriter {
readonly subagentId: string,
) {
this.logPath = getSubagentLogPath(sessionLabel, subagentId);
try {
fs.mkdirSync(getSubagentLogSessionDir(sessionLabel), { recursive: true });
} catch {
this.errored = true;
const dir = getSubagentLogSessionDir(sessionLabel);
if (!createdDirs.has(dir)) {
try {
fs.mkdirSync(dir, { recursive: true });
createdDirs.add(dir);
Comment on lines +60 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recreate directories evicted after memoization

In a long-lived process, if this session previously created a writer and its inactive log directory is later removed by the built-in retention sweep in src/agent/witness-sweep.ts, resuming the same session label leaves dir in createdDirs. Every subsequent writer therefore skips mkdirSync; createWriteStream receives ENOENT, suppresses the error, and /tasks:view silently loses all new logs for that session. Please invalidate this cache when directories are removed or retry directory creation when opening the stream reports a missing parent.

Useful? React with 👍 / 👎.

} catch {
this.errored = true;
}
}
}

Expand Down
Loading