perf(subagent-log): memoize mkdirSync per sessionLabel to avoid redundant syscalls (#1320) - #1353
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8e9c0687c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!createdDirs.has(dir)) { | ||
| try { | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| createdDirs.add(dir); |
There was a problem hiding this comment.
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 👍 / 👎.
griffinwork40
left a comment
There was a problem hiding this comment.
🤖 Automated review (hourly sweep), generated by the /review tool — a maintainer will follow up.
Review of c8e9c068 | Change type: hotfix (perf) | Regime: light | Files: 2 | Lines: ~67
Decision: MERGE — 0 blocking (2 low, 1 nit).
The optimization is sound: mkdirSync({ recursive: true }) is idempotent, the Set guard is correct, and the error path for subsequent writers degrades gracefully to the stream-error handler. Tests verify output correctness across shared sessions.
Findings
1. low · blocking: false · test-coverage · src/agent/subagent/log.test.ts:172–222 · ref: c8e9c068 · diff-context · confidence: high
The new describe('mkdirSync memoization', ...) block verifies output correctness (events round-trip) but does not assert the central claim of the PR — that mkdirSync is called once per directory instead of N times. A regression that broke the guard but preserved output correctness would pass all tests.
Evidence: The two test cases call readEvents and assert toHaveLength(1) on events — they test that outputs are correct, not that the syscall was deduplicated. grep for vi.spyOn.*mkdirSync, callCount, or toHaveBeenCalledTimes in the test file → zero matches.
Suggestion: Add vi.spyOn(fs, 'mkdirSync') and assert spy.toHaveBeenCalledTimes(1) after constructing N writers with the same sessionLabel.
2. low · blocking: false · correctness · src/agent/subagent/log.ts:62–73 · ref: c8e9c068 · diff-context · confidence: medium
The error-handling contract changes asymmetrically. The first writer per directory fails fast in the constructor (catch { this.errored = true }). Subsequent writers skip the guard entirely — if the directory is deleted between the first writer's successful mkdir and a later writer's constructor, the later writer discovers the error lazily via the stream-open 'error' handler (log.ts:114–119). The eventual outcome is the same (this.errored = true, write() no-ops), but failure latency differs.
if (!createdDirs.has(dir)) {
try { fs.mkdirSync(dir, { recursive: true }); createdDirs.add(dir); }
catch { this.errored = true; } // only fires for first writer per dir
}
// second+ writers: no errored=true path even if dir was deletedSuggestion: A brief comment documenting this asymmetry is sufficient — the lazy fallback via stream-error is correct.
3. nit · blocking: false · perf-observability · src/agent/subagent/log.ts:33 · ref: c8e9c068 · diff-context · confidence: high
The createdDirs Set has no eviction. Across a long-lived daemon process the Set grows one entry per unique sessionLabel. Growth is negligible (~30–50 char strings × sessions per process), but the comment ("this process") doesn't note the unbounded lifetime.
Suggestion: Append to the JSDoc: Grows one entry per session; not evicted (bounded by sessions per process lifetime).
What Was Not Checked
- Citations verified inline against branch HEAD
c8e9c0687c327a1867c0ebab7164c4920ee643d7. - Stated intent: PR #1353 title + body — spec-compliance assessed; no unmet intent or scope creep found.
- Did not run the test suite locally (CI shows all GitHub Actions checks green).
- Did not verify whether
getSubagentLogSessionDirreturns a deterministically normalized path — symlink resolution or trailing-slash differences could cause cache misses where the same physical directory maps to two string keys. - Did not check whether
BgJobLogWriterhas an analogous repeated-mkdir pattern that should be similarly optimized. - Merge conflict: PR is currently
CONFLICTINGagainstmain— rebase required before merge.
c8e9c06 to
6838e70
Compare
Fixes #1320.
All subagents in a session share the same log directory, so N-1
mkdirSynccalls after the first are redundantstatsyscalls on the fork hot path. Added a module-levelSet<string>to memoize created directories.Changes
src/agent/subagent/log.ts--createdDirsset gatesmkdirSyncto once-per-directorysrc/agent/subagent/log.test.ts-- 2 tests verifying correct behavior with memoization (2 writers + 8-writer stress test)Verification
pnpm lintclean