Skip to content

Release 0.30.0 - #547

Merged
mohanagy merged 4 commits into
mainfrom
release/0.30.0
Jul 14, 2026
Merged

mohanagy merged 4 commits into
mainfrom
release/0.30.0

Conversation

@mohanagy

@mohanagy mohanagy commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • add automatic MCP graph refresh for installed agent integrations
  • isolate Madar artifacts for linked Git worktrees
  • update package metadata, lockfile, SBOM, MCP Registry manifest, changelog, and README for 0.30.0

Verification

  • npm run release:verify
  • npm run registry:validate
  • npm run typecheck
  • focused Vitest release suite: 12 files, 361 tests
  • npm run build
  • npm pack --dry-run
  • npm audit --audit-level=high

After this PR merges and npm is published, push annotated tag v0.30.0 to trigger the GitHub Release workflow.

Summary by CodeRabbit

  • New Features
    • MCP integrations now automatically create and refresh workspace graphs while active files or relevant configuration change.
    • Linked Git worktrees keep graphs, caches, reports, and other artifacts isolated to prevent cross-worktree conflicts.
    • Added safer atomic graph updates and improved workspace-aware path handling.
  • Documentation
    • Updated MCP setup, CLI references, upgrade guidance, and worktree usage instructions for version 0.30.0.
  • Bug Fixes
    • Improved graph discovery and scope handling when artifacts are stored outside the active checkout.
    • Enhanced refresh recovery after interrupted processes.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b1ed451-d2fd-4151-8b8c-a037b5621a91

📥 Commits

Reviewing files that changed from the base of the PR and between 44d4561 and 9e71b96.

📒 Files selected for processing (2)
  • tests/unit/time-travel-infrastructure.test.ts
  • tests/unit/workspace.test.ts
📝 Walkthrough

Walkthrough

Madar 0.30.0 adds workspace-aware artifact routing for linked Git worktrees, automatic graph refresh for stdio MCP servers, atomic graph publication, updated MCP installation and validation, and corresponding CLI, infrastructure, test, documentation, package, and SBOM changes.

Changes

Workspace-aware artifact routing

Layer / File(s) Summary
Workspace identity and path resolution
src/shared/workspace.ts, src/shared/graph-source-root.ts, src/shared/security.ts, src/cli/parser.ts, src/infrastructure/*, src/runtime/stdio/tools.ts
Workspace and graph-root helpers resolve linked-worktree artifacts, graph paths, caches, reports, and runtime source roots consistently.
Comparisons, snapshots, federation, and reports
src/infrastructure/compare.ts, src/infrastructure/time-travel.ts, src/pipeline/federate.ts, src/infrastructure/benchmark*, tests/unit/workspace.test.ts, tests/unit/worktree-cli-artifacts.test.ts
Downstream workflows use resolved graph roots and artifact directories, including isolated temporary worktrees and external graph artifacts.

Automatic MCP graph refresh

Layer / File(s) Summary
Graph refresh and stdio lifecycle
src/infrastructure/watch.ts, src/runtime/stdio-server.ts, src/pipeline/export.ts, src/cli/main.ts, tests/unit/watch.test.ts, tests/unit/stdio-server.test.ts
Workspace watchers reconcile control-file changes, coordinate refresh leases, atomically publish graphs, and refresh active stdio sessions.
MCP installation and configuration
src/infrastructure/install.ts, src/infrastructure/doctor.ts, docs/mcp-registry/server.json, tests/unit/install.test.ts, tests/unit/doctor.test.ts
Generated MCP commands use serve --stdio --auto-refresh; hooks and doctor checks detect workspace graphs without embedded graph paths.

Release and documentation

Layer / File(s) Summary
Release metadata and documentation
CHANGELOG.md, README.md, docs/reference/cli-and-mcp.md, package.json, package-lock.json, sbom.cdx.json
Version 0.30.0 documentation, package metadata, registry guidance, and SBOM data describe the new refresh and worktree behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant MCPServer
  participant WorkspaceWatcher
  participant GraphArtifact
  Agent->>MCPServer: send stdio request
  MCPServer->>WorkspaceWatcher: reconcile active workspace
  WorkspaceWatcher->>GraphArtifact: publish refreshed graph
  MCPServer->>Agent: return graph-backed response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is generic and only states the release version; it doesn't describe the main changes in the PR. Use a concise title that names the key change, e.g. “Add MCP graph auto-refresh and linked-worktree artifact isolation”.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the main summary and verification details, and only omits optional template sections like Checklist and Related issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/0.30.0

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/mcp-response-evidence.ts (1)

129-171: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Short-circuit the fast path before reading graph.json.

readGraphSourceRoot(graphPath) still does a synchronous read/parse, but the cheap path check can already return high confidence in the common case. Move the string-only match ahead of the file read, and pass graph.graph.root_path through from the caller when it’s already available instead of rereading the artifact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/mcp-response-evidence.ts` around lines 129 - 171, The scope check
currently reads and parses graph.json before evaluating the cheap path match. In
the relevant scope-validation function, evaluate normalizedGraphPath against
expectedGraphPath first and return high confidence immediately when it matches;
otherwise use an optional caller-provided graph.graph.root_path for source-root
validation, falling back to readGraphSourceRoot only when that value is
unavailable. Update the caller to pass the existing root_path through without
rereading the artifact.
🧹 Nitpick comments (4)
src/infrastructure/watch.ts (1)

167-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate sameFilesystemPath production helper across infrastructure and runtime layers. Both files independently implement identical filesystem-identity comparison logic used to validate that a graph artifact belongs to the expected workspace; the shared root cause is that this utility has no single source of truth, risking silent drift between the two copies for a security/correctness-relevant check.

  • src/infrastructure/watch.ts#L167-L173: keep the canonical implementation here (or move it into src/shared/workspace.ts, which both files already import) and export it.
  • src/runtime/stdio-server.ts#L154-L160: remove this local copy and import sameFilesystemPath from src/infrastructure/watch.ts (or the shared module) instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/infrastructure/watch.ts` around lines 167 - 173, Make sameFilesystemPath
a single exported implementation in src/infrastructure/watch.ts (or
shared/workspace.ts), preserving its realpathSync/resolve behavior; in
src/runtime/stdio-server.ts, remove the duplicate helper and import the shared
symbol instead.
tests/unit/watch.test.ts (1)

644-654: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate waitFor polling helper across two test files. Both files independently define a byte-for-byte equivalent async condition-poller for waiting on graph-refresh side effects; the shared root cause is that this test utility has no common home.

  • tests/unit/watch.test.ts#L644-L654: remove this local waitFor and import it from a shared test-utils module instead.
  • tests/unit/stdio-server.test.ts#L12-L22: remove this local waitFor and import it from the same shared test-utils module instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/watch.test.ts` around lines 644 - 654, The duplicated async
waitFor polling helper should live in a shared test-utils module. Remove the
local waitFor definition from tests/unit/watch.test.ts at lines 644-654 and
import the shared helper; likewise remove the local definition from
tests/unit/stdio-server.test.ts at lines 12-22 and import that same helper,
preserving existing call behavior.
src/runtime/task-applicability.ts (1)

364-396: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Same worktree/graph-detection heuristic hand-duplicated in three places. hasMadarGraph/WORKSPACE_GRAPH_CHECK independently re-implement "walk up from cwd checking for out/graph.json or a .git file (linked worktree)" in three different syntaxes (CommonJS, ESM, and a compact node -e one-liner). A future change to this detection logic (e.g. handling git submodules that also use a .git file, or adding another worktree signal) risks being applied to only one or two of the three copies, silently diverging hook behavior across agent platforms.

  • src/runtime/task-applicability.ts#L364-L396: treat this as the canonical multi-line implementation; extract it into a single shared string-template builder that the other two sites derive their compacted/ESM variants from.
  • src/infrastructure/install.ts#L131-L142: generate WORKSPACE_GRAPH_CHECK's compact one-liner from the same shared template instead of a hand-minified copy.
  • src/infrastructure/install.ts#L619-L644: generate OPENCODE_PLUGIN_JS's hasMadarGraph from the same shared template (ESM-flavored) instead of a hand-written copy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/task-applicability.ts` around lines 364 - 396, Extract the
canonical worktree/graph-detection logic from task-applicability.ts#L364-L396
into a shared string-template builder, then derive all variants from it. Update
src/runtime/task-applicability.ts#L364-L396,
src/infrastructure/install.ts#L131-L142 (WORKSPACE_GRAPH_CHECK), and
src/infrastructure/install.ts#L619-L644 (OPENCODE_PLUGIN_JS) so their CommonJS,
compact, and ESM forms share the same walk-up detection behavior.
src/pipeline/federate.ts (1)

39-64: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid re-reading the graph file just to infer the repo name.

loadSourceGraph already parses the JSON; thread parsed.root_path through to inferRepoName (or pass a pre-parsed carrier into resolveGraphSourceRoot) instead of calling readGraphSourceRoot(graphPath) again. That removes an extra disk read and JSON parse on the federate path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/federate.ts` around lines 39 - 64, Update loadSourceGraph to
return the parsed root_path alongside the KnowledgeGraph, then change
inferRepoName to use that parsed value instead of calling
readGraphSourceRoot(graphPath). Propagate the new value through the federate
path so repository-name inference reuses the existing parse and avoids another
file read.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/infrastructure/watch.ts`:
- Around line 175-193: Merge graphBelongsToWorkspace and graphUsesSpi into a
single graph metadata read that performs one readFileSync and JSON.parse, then
returns both the workspace-membership and SPI-mode values. Update rebuildCode’s
generateGraph options construction to consume those results instead of invoking
both independent helpers, while preserving the existing false behavior for
unreadable or invalid graph files.

In `@src/pipeline/export.ts`:
- Around line 381-394: Update writeFileAtomically to retry renameSync when
Windows transient lock errors EPERM or EBUSY occur, using a small bounded
backoff between attempts. Preserve same-directory atomic replacement, rethrow
non-transient errors and the lock error after retries are exhausted, and retain
temporary-file cleanup in the finally block.

In `@src/runtime/stdio-server.ts`:
- Around line 162-170: Update graphRootPath to avoid the uncached loadGraph and
KnowledgeGraph rebuild on each tools/list request; reuse the existing cached
graph-loading path, such as loadGraphCached, while preserving validation and the
current null-on-failure behavior when reading graph.graph.root_path.

In `@src/shared/workspace.ts`:
- Around line 30-41: Memoize resolveMadarWorkspace by resolved rootPath so
repeated cacheDir lookups reuse the previously computed workspace instead of
rerunning its git commands. Add caching at the resolveMadarWorkspace boundary,
preserve its existing result and failure behavior, and ensure different rootPath
values remain independently cached.

---

Outside diff comments:
In `@src/runtime/mcp-response-evidence.ts`:
- Around line 129-171: The scope check currently reads and parses graph.json
before evaluating the cheap path match. In the relevant scope-validation
function, evaluate normalizedGraphPath against expectedGraphPath first and
return high confidence immediately when it matches; otherwise use an optional
caller-provided graph.graph.root_path for source-root validation, falling back
to readGraphSourceRoot only when that value is unavailable. Update the caller to
pass the existing root_path through without rereading the artifact.

---

Nitpick comments:
In `@src/infrastructure/watch.ts`:
- Around line 167-173: Make sameFilesystemPath a single exported implementation
in src/infrastructure/watch.ts (or shared/workspace.ts), preserving its
realpathSync/resolve behavior; in src/runtime/stdio-server.ts, remove the
duplicate helper and import the shared symbol instead.

In `@src/pipeline/federate.ts`:
- Around line 39-64: Update loadSourceGraph to return the parsed root_path
alongside the KnowledgeGraph, then change inferRepoName to use that parsed value
instead of calling readGraphSourceRoot(graphPath). Propagate the new value
through the federate path so repository-name inference reuses the existing parse
and avoids another file read.

In `@src/runtime/task-applicability.ts`:
- Around line 364-396: Extract the canonical worktree/graph-detection logic from
task-applicability.ts#L364-L396 into a shared string-template builder, then
derive all variants from it. Update src/runtime/task-applicability.ts#L364-L396,
src/infrastructure/install.ts#L131-L142 (WORKSPACE_GRAPH_CHECK), and
src/infrastructure/install.ts#L619-L644 (OPENCODE_PLUGIN_JS) so their CommonJS,
compact, and ESM forms share the same walk-up detection behavior.

In `@tests/unit/watch.test.ts`:
- Around line 644-654: The duplicated async waitFor polling helper should live
in a shared test-utils module. Remove the local waitFor definition from
tests/unit/watch.test.ts at lines 644-654 and import the shared helper; likewise
remove the local definition from tests/unit/stdio-server.test.ts at lines 12-22
and import that same helper, preserving existing call behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4987768d-b542-4c17-b404-8dc8ff4cc841

📥 Commits

Reviewing files that changed from the base of the PR and between a5021be and 44d4561.

📒 Files selected for processing (47)
  • .github/scripts/validate-mcp-registry.mjs
  • CHANGELOG.md
  • README.md
  • docs/mcp-registry/server.json
  • docs/reference/cli-and-mcp.md
  • package-lock.json
  • package.json
  • sbom.cdx.json
  • src/cli/main.ts
  • src/cli/parser.ts
  • src/infrastructure/benchmark.ts
  • src/infrastructure/benchmark/quality.ts
  • src/infrastructure/benchmark/runner.ts
  • src/infrastructure/cache.ts
  • src/infrastructure/compare.ts
  • src/infrastructure/doctor.ts
  • src/infrastructure/generate.ts
  • src/infrastructure/handoff-command.ts
  • src/infrastructure/install.ts
  • src/infrastructure/proof-report.ts
  • src/infrastructure/review-compare.ts
  • src/infrastructure/time-travel.ts
  • src/infrastructure/try-command.ts
  • src/infrastructure/watch.ts
  • src/pipeline/export.ts
  • src/pipeline/federate.ts
  • src/pipeline/spi/cache.ts
  • src/runtime/mcp-response-evidence.ts
  • src/runtime/stdio-server.ts
  • src/runtime/stdio/tools.ts
  • src/runtime/task-applicability.ts
  • src/shared/graph-source-root.ts
  • src/shared/security.ts
  • src/shared/workspace.ts
  • tests/unit/cli.test.ts
  • tests/unit/compare-native-agent.test.ts
  • tests/unit/doctor.test.ts
  • tests/unit/install.test.ts
  • tests/unit/mcp-registry-metadata.test.ts
  • tests/unit/mcp-response-evidence.test.ts
  • tests/unit/pipeline.test.ts
  • tests/unit/stdio-pr-impact.test.ts
  • tests/unit/stdio-server.test.ts
  • tests/unit/time-travel-infrastructure.test.ts
  • tests/unit/watch.test.ts
  • tests/unit/workspace.test.ts
  • tests/unit/worktree-cli-artifacts.test.ts

Comment on lines +175 to +193
function graphBelongsToWorkspace(graphPath: string, workspaceRoot: string): boolean {
try {
const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { root_path?: unknown }
return typeof parsed.root_path === 'string'
&& parsed.root_path.trim().length > 0
&& sameFilesystemPath(parsed.root_path, workspaceRoot)
} catch {
return false
}
}

function graphUsesSpi(graphPath: string): boolean {
try {
const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { spi_mode?: unknown }
return parsed.spi_mode === true
} catch {
return false
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Two full, separate graph.json parses per rebuild just to read two scalar fields.

graphBelongsToWorkspace and graphUsesSpi each independently readFileSync + JSON.parse the entire graph.json. Both are called back-to-back while building generateGraph's options in rebuildCode (lines 449-451), so every single debounced rebuild during an active watch/auto-refresh session fully re-parses the same large file twice just to read root_path and spi_mode. For the large repos this tool targets, that's a repeatable, avoidable I/O+CPU cost on a hot path.

🐛 Proposed fix: merge into one read
-function graphBelongsToWorkspace(graphPath: string, workspaceRoot: string): boolean {
-  try {
-    const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { root_path?: unknown }
-    return typeof parsed.root_path === 'string'
-      && parsed.root_path.trim().length > 0
-      && sameFilesystemPath(parsed.root_path, workspaceRoot)
-  } catch {
-    return false
-  }
-}
-
-function graphUsesSpi(graphPath: string): boolean {
-  try {
-    const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { spi_mode?: unknown }
-    return parsed.spi_mode === true
-  } catch {
-    return false
-  }
-}
+function readGraphRefreshMetadata(graphPath: string): { rootPath: string | null; usesSpi: boolean } {
+  try {
+    const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { root_path?: unknown; spi_mode?: unknown }
+    return {
+      rootPath: typeof parsed.root_path === 'string' && parsed.root_path.trim().length > 0 ? parsed.root_path.trim() : null,
+      usesSpi: parsed.spi_mode === true,
+    }
+  } catch {
+    return { rootPath: null, usesSpi: false }
+  }
+}

And at the call site:

-      result = generateGraph(resolvedWatchPath, {
-        ...(existsSync(manifestPath) && existsSync(graphPath) && graphBelongsToWorkspace(graphPath, resolvedWatchPath) ? { update: true } : {}),
-        ...(graphUsesSpi(graphPath) ? { useSpi: true } : {}),
+      const existingGraphMetadata = existsSync(manifestPath) && existsSync(graphPath) ? readGraphRefreshMetadata(graphPath) : null
+      result = generateGraph(resolvedWatchPath, {
+        ...(existingGraphMetadata?.rootPath && sameFilesystemPath(existingGraphMetadata.rootPath, resolvedWatchPath) ? { update: true } : {}),
+        ...(existingGraphMetadata?.usesSpi ? { useSpi: true } : {}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/infrastructure/watch.ts` around lines 175 - 193, Merge
graphBelongsToWorkspace and graphUsesSpi into a single graph metadata read that
performs one readFileSync and JSON.parse, then returns both the
workspace-membership and SPI-mode values. Update rebuildCode’s generateGraph
options construction to consume those results instead of invoking both
independent helpers, while preserving the existing false behavior for unreadable
or invalid graph files.

Comment thread src/pipeline/export.ts
Comment on lines +381 to +394
function writeFileAtomically(outputPath: string, content: string): void {
const temporaryPath = join(
dirname(outputPath),
`.madar-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`,
)

try {
writeFileSync(temporaryPath, content, 'utf8')
renameSync(temporaryPath, outputPath)
} finally {
rmSync(temporaryPath, { force: true })
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Node.js fs.renameSync throw EPERM or EBUSY on Windows when the destination file is open for reading in another process, and is there a recommended retry pattern?

💡 Result:

Yes, Node.js fs.renameSync (and its asynchronous counterpart fs.rename) will frequently throw EPERM or EBUSY errors on Windows when the destination file or source file is held open by another process [1][2][3][4]. Unlike POSIX systems (Linux/macOS), where renaming is generally an atomic operation that can proceed even if a file is open, Windows enforces mandatory file locking [2][4]. If any process—including antivirus software, the Windows Search indexer, a text editor, or another instance of Node.js—has an active handle on the file, the operating system will block the rename operation, and Node.js will surface this as an EPERM or EBUSY error [2][5][6][4]. Recommended Retry Pattern Because these locks are often transient (e.g., an antivirus scan finishing or a file being released by a background process), a common and recommended solution is to implement a retry mechanism with exponential backoff [2]. 1. Manual Implementation: You can wrap the fs.renameSync call in a loop with a small delay [6]. Example (Conceptual): javascript const fs = require('fs'); function renameWithRetry(src, dest, retries = 5, delay = 100) { for (let i = 0; i < retries; i++) { try { fs.renameSync(src, dest); return; // Success } catch (err) { if ((err.code === 'EPERM' || err.code === 'EBUSY') && i < retries - 1) { // Wait before retrying const start = Date.now(); while (Date.now() - start < delay) {} delay *= 2; // Exponential backoff } else { throw err; // Rethrow if not a transient error or retries exhausted } } } } 2. Using graceful-fs: The community-maintained library graceful-fs is the standard approach to resolve this issue [3][6][4]. It automatically patches the Node.js fs module on Windows to include an internal retry mechanism for EPERM, EACCES, and EBUSY errors, sparing you from implementing manual logic [3][4]. Best Practice: - If the file lock is held by your own application (e.g., a file stream that hasn't finished writing), ensure the stream is fully closed—typically by waiting for the 'close' or 'end' event—before attempting to rename [2]. - Use the graceful-fs library if you are working in an environment where third-party file locks (like antivirus) are unpredictable [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols.
ast-grep outline src/pipeline/export.ts --view expanded || true

# Show the relevant section with line numbers.
sed -n '350,430p' src/pipeline/export.ts | cat -n

# Search for other rename/write atomic helpers or retry logic in the repo.
rg -n "renameSync|writeFileAtomically|EBUSY|EPERM|retry|backoff|graceful-fs" src . --glob '!**/node_modules/**' || true

Repository: mohanagy/madar

Length of output: 41645


Retry renameSync on Windows transient lock errors. The same-directory rename keeps the JSON write atomic, but it can still throw EPERM/EBUSY on Windows when another process briefly holds the target file. A small retry/backoff here would keep exports from failing on a transient lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/export.ts` around lines 381 - 394, Update writeFileAtomically to
retry renameSync when Windows transient lock errors EPERM or EBUSY occur, using
a small bounded backoff between attempts. Preserve same-directory atomic
replacement, rethrow non-transient errors and the lock error after retries are
exhausted, and retain temporary-file cleanup in the finally block.

Comment on lines +162 to +170
function graphRootPath(graphPath: string): string | null {
try {
const graph = loadGraph(validateGraphPath(graphPath))
const rootPath = graph.graph.root_path
return typeof rootPath === 'string' && rootPath.trim().length > 0 ? rootPath.trim() : null
} catch {
return null
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

graphRootPath does a full uncached graph load on every tools/list call.

graphRootPath calls loadGraph(validateGraphPath(graphPath)) - a full JSON parse + KnowledgeGraph rebuild - purely to read the root_path string field. It's invoked on every tools/list request (line 669), whereas the same diff's compareRefs override (line 696) correctly reuses loadGraphCached. For large graphs (this tool's stated target: large TS/Node repos) this adds unnecessary parse/build latency to a request that previously only did a cheap dirname(graphPath) lookup.

🐛 Proposed fix
 function graphRootPath(graphPath: string): string | null {
   try {
-    const graph = loadGraph(validateGraphPath(graphPath))
+    const graph = loadGraphCached(graphPath)
     const rootPath = graph.graph.root_path
     return typeof rootPath === 'string' && rootPath.trim().length > 0 ? rootPath.trim() : null
   } catch {
     return null
   }
 }

Also applies to: 669-669

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/stdio-server.ts` around lines 162 - 170, Update graphRootPath to
avoid the uncached loadGraph and KnowledgeGraph rebuild on each tools/list
request; reuse the existing cached graph-loading path, such as loadGraphCached,
while preserving validation and the current null-on-failure behavior when
reading graph.graph.root_path.

Comment thread src/shared/workspace.ts
Comment on lines +30 to +41
function gitPath(rootPath: string, args: string[]): string | null {
try {
const value = execFileSync('git', ['-C', rootPath, ...args], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
}).trim()
return value.length > 0 ? value : null
} catch {
return null
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## files\n'
git ls-files src/shared/workspace.ts src/infrastructure/cache.ts

printf '\n## outline workspace.ts\n'
ast-grep outline src/shared/workspace.ts --view expanded || true

printf '\n## outline cache.ts\n'
ast-grep outline src/infrastructure/cache.ts --view expanded || true

printf '\n## relevant excerpts\n'
sed -n '1,220p' src/shared/workspace.ts | cat -n
printf '\n--- cache.ts ---\n'
sed -n '1,240p' src/infrastructure/cache.ts | cat -n

Repository: mohanagy/madar

Length of output: 11944


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- workspace.ts lines 1-180 ---'
sed -n '1,180p' src/shared/workspace.ts | cat -n

echo '--- cache.ts lines 1-220 ---'
sed -n '1,220p' src/infrastructure/cache.ts | cat -n

Repository: mohanagy/madar

Length of output: 10305


Memoize resolveMadarWorkspace
resolveMadarWorkspace() runs up to three synchronous git commands on every call, and src/infrastructure/cache.ts’s cacheDir() reaches it on each cache lookup. Cache the workspace by resolved rootPath to avoid repeated subprocess spawns on large repos.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/workspace.ts` around lines 30 - 41, Memoize resolveMadarWorkspace
by resolved rootPath so repeated cacheDir lookups reuse the previously computed
workspace instead of rerunning its git commands. Add caching at the
resolveMadarWorkspace boundary, preserve its existing result and failure
behavior, and ensure different rootPath values remain independently cached.

@mohanagy
mohanagy merged commit 0b5aa7c into main Jul 14, 2026
6 checks passed
@mohanagy
mohanagy deleted the release/0.30.0 branch July 14, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant