Skip to content

perf(isolator): run per-capsule installs sequentially instead of all at once#10457

Open
davidfirst wants to merge 4 commits into
masterfrom
fix-isolator-batch-capsule-install
Open

perf(isolator): run per-capsule installs sequentially instead of all at once#10457
davidfirst wants to merge 4 commits into
masterfrom
fix-isolator-batch-capsule-install

Conversation

@davidfirst

@davidfirst davidfirst commented Jun 26, 2026

Copy link
Copy Markdown
Member

Scope-aspect/env capsules are each installed in their own package-manager install. They were all fired at once via Promise.all, but these installs serialize on the pnpm store lock, so full concurrency mostly thrashes (CPU oversubscription + lock contention) and ends up slower than running them one at a time.

This caps the per-capsule install concurrency to 1 (sequential). On a cold rebuild of 5 env capsules it's both faster and far lighter on CPU:

  • Promise.all (all at once): ~7m41s
  • sequential: ~6m18s, with a large drop in CPU/user time (~9m30s → ~1m39s in my runs)

Lower CPU matters on CI where other jobs share the box. Isolation is unchanged — these are the exact same per-capsule installs, just throttled (kept as a named constant so the value is easy to tune).

Why not batch into one install

The original idea here was to batch all capsules into a single install to skip the repeated resolution. That breaks per-capsule isolation: capsules are loaded and executed at runtime, so each must resolve its own dependency tree (including peers like React) to the exact versions it needs. A single shared install hoists/unifies deps across capsules — caught by the deps-in-capsules and root-components e2e tests (e.g. two versions of the same aspect resolving the wrong React). Direct-dep placement was fixable with pnpm hoisting flags, but per-capsule peer/version isolation wasn't, so the batching approach was dropped in favor of this safe concurrency change.

When rebuilding scope-aspects capsules with nesting enabled, each capsule
was installed via its own separate package-manager install. For a cold
rebuild of many capsules this means N separate installs that each re-resolve
a heavily-overlapping dependency tree and serialize on the pnpm store lock
(~6m for 5 env capsules). When more than one capsule needs installing, fall
back to a single batched install; capsule isolation is preserved via
rootComponentsForCapsules (~1m for the same 5 capsules). The single-capsule
incremental path is unchanged.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Batch-install capsules when nesting enabled to avoid N installs
🐞 Bug fix ✨ Enhancement 🕐 10-20 Minutes

Grey Divider

Description

• Batch package installs when nesting is enabled and multiple capsules are rebuilt.
• Preserve per-capsule nesting install behavior for single-capsule incremental rebuilds.
• Pass nodeLinker through the single-capsule install path for consistent installer configuration.
Diagram

graph TD
  A["IsolatorMain.createCapsules"] --> B["linkInCapsulesRoot"] --> C{"useNesting & single capsule?"}
  C -- "yes" --> D["linkInCapsules (1)"] --> E["installInCapsules (per-capsule)"] --> F["DependencyResolver installer"]
  C -- "no" --> G["linkInCapsules (batch)"] --> H["installInCapsules (batched)"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always use batched installs (even for 1 capsule)
  • ➕ Simpler logic; one consistent install path
  • ➕ Avoids any chance of reintroducing per-capsule install overhead
  • ➖ Can regress the incremental case where only one capsule is missing/added
  • ➖ May do more work than necessary for small updates
2. Make batching threshold configurable (e.g., batch when >= K capsules)
  • ➕ Allows tuning per environment/CI vs local workflows
  • ➕ Can balance incremental latency vs cold-rebuild throughput
  • ➖ Adds configuration surface area and support burden
  • ➖ Harder to pick a default and reason about behavior across setups
3. Keep per-capsule nesting but attempt parallelism/caching improvements
  • ➕ Preserves strict per-capsule execution model
  • ➕ Could improve cold rebuilds without changing strategy selection
  • ➖ Limited benefit with pnpm store locking/overlapping resolution; still repeats work N times
  • ➖ More complex and less reliable than reducing installs to one

Recommendation: The chosen approach (nesting per-capsule only when a single capsule is being installed; otherwise batch) is the best trade-off: it keeps the fast incremental behavior while eliminating the dominant cold-rebuild cost from repeated dependency resolution and pnpm store-lock serialization. A configurable threshold could be considered later if real-world workflows show a different crossover point, but the current condition ("more than one") is a pragmatic default.

Files changed (1) +10 / -1

Bug fix (1) +10 / -1
isolator.main.runtime.tsBatch installs for multi-capsule nesting; propagate nodeLinker in single-capsule path +10/-1

Batch installs for multi-capsule nesting; propagate nodeLinker in single-capsule path

• Changes the nesting install behavior to run per-capsule installs only when exactly one capsule needs installation; otherwise it uses a single batched install across capsules to avoid repeated resolution and pnpm store-lock contention. Also passes 'nodeLinker' into the per-capsule 'installInCapsules' call so installer configuration matches the batched path.

scopes/component/isolator/isolator.main.runtime.ts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 26, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Empty nesting triggers install ✓ Resolved 🐞 Bug ➹ Performance
Description
When useNesting is enabled and skipIfExists filters out all capsules, capsuleList can become empty
but the new condition only special-cases length===1; length===0 now falls into the batched path and
still calls installInCapsules(). This can cause an unnecessary package-manager install in the
capsules root, adding latency and potentially mutating root lockfiles/node_modules even though
nothing needed installation.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
  await Promise.all(
    capsuleList.map(async (capsule, index) => {
      const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
skipIfExists with nesting can reduce capsuleList to an empty list, but the new nesting condition
only handles the single-capsule case. The else branch still invokes installInCapsules(...),
which unconditionally calls into the dependency installer and then the package manager install, so
an empty capsule list still triggers a full install.

scopes/component/isolator/isolator.main.runtime.ts[737-753]
scopes/component/isolator/isolator.main.runtime.ts[769-833]
scopes/component/isolator/isolator.main.runtime.ts[944-1000]
scopes/dependencies/dependency-resolver/dependency-installer.ts[124-153]
scopes/dependencies/dependency-resolver/dependency-installer.ts[273-281]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
With `useNesting` enabled, the code now only uses the per-capsule nesting path when `capsuleList.length === 1`. If `capsuleList.length === 0` (e.g. `skipIfExists` filtered out all capsules), execution falls into the `else` branch and still runs `installInCapsules(...)`, triggering a package-manager install even though there are no capsules to install.
### Issue Context
This is a behavioral change introduced by the new `capsuleList.length === 1` condition. Previously, the nesting branch would no-op on an empty list (because it mapped over capsules), avoiding an expensive install.
### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[737-833]
### Suggested fix
Add an explicit guard to skip installation when there are no capsules to install, e.g.:
- If `installOptions.useNesting && capsuleList.length === 0`: skip `linkInCapsules(...)` / `installInCapsules(...)` and proceed.
- Keep existing behavior for `capsuleList.length === 1` (per-capsule install) and `> 1` (batched install).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Nested installs always serial 🐞 Bug ➹ Performance ⭐ New
Description
Nested capsule installs are now globally capped to concurrency=1 via
NESTED_CAPSULE_INSTALL_CONCURRENCY, regardless of which package manager is configured. This forces
one-by-one installs in nesting mode (including when packageManager is yarn), which can increase
total rebuild time compared to the previous concurrent behavior.
Code

scopes/component/isolator/isolator.main.runtime.ts[R109-113]

+/**
+ * How many per-capsule nested installs to run at once. These installs are heavy and serialize on the
+ * pnpm store lock, so unbounded concurrency (Promise.all) mostly thrashes. A small cap performs better.
+ */
+const NESTED_CAPSULE_INSTALL_CONCURRENCY = 1;
Evidence
The PR introduces a new constant set to 1 and uses it to cap pMap() concurrency for all nesting
installs, while the install path still forwards the selected packageManager (so the cap affects
yarn as well). The repository also enables useNesting: true by default for scope-aspect capsules,
and Yarn’s package manager implementation has explicit branching for useNesting, indicating this
mode is expected to run under yarn too.

scopes/component/isolator/isolator.main.runtime.ts[109-116]
scopes/component/isolator/isolator.main.runtime.ts[853-917]
scopes/component/isolator/isolator.main.runtime.ts[1048-1066]
scopes/scope/scope/scope-aspects-loader.ts[494-511]
scopes/dependencies/yarn/yarn.package-manager.ts[53-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`NESTED_CAPSULE_INSTALL_CONCURRENCY` is hard-coded to `1` and is used to throttle nested capsule installs. This applies to all package managers, even though the rationale in comments is pnpm-specific (store-lock contention).

### Issue Context
- `createCapsules()` now runs nesting installs through `pMap(..., { concurrency: NESTED_CAPSULE_INSTALL_CONCURRENCY })`.
- `installInCapsules()` forwards `opts.packageManager`, and nesting is enabled by default for scope-aspect capsules.
- Yarn has specific behavior for `useNesting`, so forcing serialized installs may be an unnecessary slowdown when nesting is used with yarn.

### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[109-116]
- scopes/component/isolator/isolator.main.runtime.ts[863-917]

### Suggested fix
- Replace the hard-coded `1` with either:
 - a configurable option (e.g. `opts.installOptions?.nestedInstallConcurrency`), **or**
 - a package-manager-aware value (e.g. keep `1` for pnpm, but use a higher limit for yarn/npm), **or**
 - fall back to an existing concurrency control like `concurrentComponentsLimit()` when not pnpm.
- Keep the current behavior as the default only if explicitly intended for all package managers (in which case, add an explicit comment/guard to avoid accidental regressions).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Lockfile cache bypassed 🐞 Bug ☼ Reliability
Description
The cacheLockFileOnly lockfile reuse/caching logic is now only executed in the single-capsule
nesting path; multi-capsule nesting installs take the batched branch and skip the per-capsule
lockfile copy. The process-exit cache flow still looks for per-capsule pnpm-lock.yaml files, but the
batched pnpm flow is keyed by a single rootDir, so lockfile-only caching may become
ineffective/no-op for multi-capsule nesting installs.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
  await Promise.all(
    capsuleList.map(async (capsule, index) => {
      const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
The per-capsule lockfile copy is inside the single-capsule nesting branch only. The cache-on-exit
path moves pnpm-lock.yaml from each capsule directory, but pnpm’s install/lockfile bookkeeping is
driven by the single rootDir passed to the installer, making per-capsule lockfile caching unlikely
to work in the batched branch where rootDir is capsulesDir.

scopes/component/isolator/isolator.main.runtime.ts[782-799]
scopes/component/isolator/isolator.main.runtime.ts[812-827]
scopes/component/isolator/isolator.main.runtime.ts[488-529]
scopes/dependencies/pnpm/lynx.ts[292-353]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `useNesting` is enabled and more than one capsule is installed, the code now uses the batched install path. This path skips the existing per-capsule `cacheLockFileOnly` lockfile-copy optimization, while the cache-on-exit logic still expects `pnpm-lock.yaml` inside each capsule directory.
### Issue Context
The lockfile caching flow (`cacheLockFileOnly`) copies/moves `pnpm-lock.yaml` files per capsule directory. The new batching path installs with `rootDir = capsulesDir`, while pnpm lockfile operations are keyed by the single `rootDir`, so the per-capsule lockfile cache mechanism may not be used for multi-capsule nesting installs.
### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[778-833]
- scopes/component/isolator/isolator.main.runtime.ts[488-529]
### Suggested fix
Choose one consistent strategy for `cacheLockFileOnly` when batching nesting installs:
- Either (A) implement copying/caching of the *root* lockfile (e.g. `${capsulesDir}/pnpm-lock.yaml`) when running the batched install, and update the cache-move code to handle that root lockfile, or
- (B) if per-capsule lockfiles are still required for caching, ensure the batched nesting install produces/uses them (or keep per-capsule installs when `cacheLockFileOnly` is enabled).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 262ff2c

Results up to commit 3dba919


🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)


Action required
1. Empty nesting triggers install ✓ Resolved 🐞 Bug ➹ Performance
Description
When useNesting is enabled and skipIfExists filters out all capsules, capsuleList can become empty
but the new condition only special-cases length===1; length===0 now falls into the batched path and
still calls installInCapsules(). This can cause an unnecessary package-manager install in the
capsules root, adding latency and potentially mutating root lockfiles/node_modules even though
nothing needed installation.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
    await Promise.all(
      capsuleList.map(async (capsule, index) => {
        const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
skipIfExists with nesting can reduce capsuleList to an empty list, but the new nesting condition
only handles the single-capsule case. The else branch still invokes installInCapsules(...),
which unconditionally calls into the dependency installer and then the package manager install, so
an empty capsule list still triggers a full install.

scopes/component/isolator/isolator.main.runtime.ts[737-753]
scopes/component/isolator/isolator.main.runtime.ts[769-833]
scopes/component/isolator/isolator.main.runtime.ts[944-1000]
scopes/dependencies/dependency-resolver/dependency-installer.ts[124-153]
scopes/dependencies/dependency-resolver/dependency-installer.ts[273-281]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
With `useNesting` enabled, the code now only uses the per-capsule nesting path when `capsuleList.length === 1`. If `capsuleList.length === 0` (e.g. `skipIfExists` filtered out all capsules), execution falls into the `else` branch and still runs `installInCapsules(...)`, triggering a package-manager install even though there are no capsules to install.
### Issue Context
This is a behavioral change introduced by the new `capsuleList.length === 1` condition. Previously, the nesting branch would no-op on an empty list (because it mapped over capsules), avoiding an expensive install.
### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[737-833]
### Suggested fix
Add an explicit guard to skip installation when there are no capsules to install, e.g.:
- If `installOptions.useNesting && capsuleList.length === 0`: skip `linkInCapsules(...)` / `installInCapsules(...)` and proceed.
- Keep existing behavior for `capsuleList.length === 1` (per-capsule install) and `> 1` (batched install).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Lockfile cache bypassed 🐞 Bug ☼ Reliability
Description
The cacheLockFileOnly lockfile reuse/caching logic is now only executed in the single-capsule
nesting path; multi-capsule nesting installs take the batched branch and skip the per-capsule
lockfile copy. The process-exit cache flow still looks for per-capsule pnpm-lock.yaml files, but the
batched pnpm flow is keyed by a single rootDir, so lockfile-only caching may become
ineffective/no-op for multi-capsule nesting installs.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
    await Promise.all(
      capsuleList.map(async (capsule, index) => {
        const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
The per-capsule lockfile copy is inside the single-capsule nesting branch only. The cache-on-exit
path moves pnpm-lock.yaml from each capsule directory, but pnpm’s install/lockfile bookkeeping is
driven by the single rootDir passed to the installer, making per-capsule lockfile caching unlikely
to work in the batched branch where rootDir is capsulesDir.

scopes/component/isolator/isolator.main.runtime.ts[782-799]
scopes/component/isolator/isolator.main.runtime.ts[812-827]
scopes/component/isolator/isolator.main.runtime.ts[488-529]
scopes/dependencies/pnpm/lynx.ts[292-353]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `useNesting` is enabled and more than one capsule is installed, the code now uses the batched install path. This path skips the existing per-capsule `cacheLockFileOnly` lockfile-copy optimization, while the cache-on-exit logic still expects `pnpm-lock.yaml` inside each capsule directory.
### Issue Context
The lockfile caching flow (`cacheLockFileOnly`) copies/moves `pnpm-lock.yaml` files per capsule directory. The new batching path installs with `rootDir = capsulesDir`, while pnpm lockfile operations are keyed by the single `rootDir`, so the per-capsule lockfile cache mechanism may not be used for multi-capsule nesting installs.
### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[778-833]
- scopes/component/isolator/isolator.main.runtime.ts[488-529]
### Suggested fix
Choose one consistent strategy for `cacheLockFileOnly` when batching nesting installs:
- Either (A) implement copying/caching of the *root* lockfile (e.g. `${capsulesDir}/pnpm-lock.yaml`) when running the batched install, and update the cache-move code to handle that root lockfile, or
- (B) if per-capsule lockfiles are still required for caching, ensure the batched nesting install produces/uses them (or keep per-capsule installs when `cacheLockFileOnly` is enabled).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit dfd76c7


🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)


Action required
1. Empty nesting triggers install ✓ Resolved 🐞 Bug ➹ Performance
Description
When useNesting is enabled and skipIfExists filters out all capsules, capsuleList can become empty
but the new condition only special-cases length===1; length===0 now falls into the batched path and
still calls installInCapsules(). This can cause an unnecessary package-manager install in the
capsules root, adding latency and potentially mutating root lockfiles/node_modules even though
nothing needed installation.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
      await Promise.all(
        capsuleList.map(async (capsule, index) => {
          const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
skipIfExists with nesting can reduce capsuleList to an empty list, but the new nesting condition
only handles the single-capsule case. The else branch still invokes installInCapsules(...),
which unconditionally calls into the dependency installer and then the package manager install, so
an empty capsule list still triggers a full install.

scopes/component/isolator/isolator.main.runtime.ts[737-753]
scopes/component/isolator/isolator.main.runtime.ts[769-833]
scopes/component/isolator/isolator.main.runtime.ts[944-1000]
scopes/dependencies/dependency-resolver/dependency-installer.ts[124-153]
scopes/dependencies/dependency-resolver/dependency-installer.ts[273-281]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
With `useNesting` enabled, the code now only uses the per-capsule nesting path when `capsuleList.length === 1`. If `capsuleList.length === 0` (e.g. `skipIfExists` filtered out all capsules), execution falls into the `else` branch and still runs `installInCapsules(...)`, triggering a package-manager install even though there are no capsules to install.
### Issue Context
This is a behavioral change introduced by the new `capsuleList.length === 1` condition. Previously, the nesting branch would no-op on an empty list (because it mapped over capsules), avoiding an expensive install.
### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[737-833]
### Suggested fix
Add an explicit guard to skip installation when there are no capsules to install, e.g.:
- If `installOptions.useNesting && capsuleList.length === 0`: skip `linkInCapsules(...)` / `installInCapsules(...)` and proceed.
- Keep existing behavior for `capsuleList.length === 1` (per-capsule install) and `> 1` (batched install).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Lockfile cache bypassed 🐞 Bug ☼ Reliability
Description
The cacheLockFileOnly lockfile reuse/caching logic is now only executed in the single-capsule
nesting path; multi-capsule nesting installs take the batched branch and skip the per-capsule
lockfile copy. The process-exit cache flow still looks for per-capsule pnpm-lock.yaml files, but the
batched pnpm flow is keyed by a single rootDir, so lockfile-only caching may become
ineffective/no-op for multi-capsule nesting installs.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
      await Promise.all(
        capsuleList.map(async (capsule, index) => {
          const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
The per-capsule lockfile copy is inside the single-capsule nesting branch only. The cache-on-exit
path moves pnpm-lock.yaml from each capsule directory, but pnpm’s install/lockfile bookkeeping is
driven by the single rootDir passed to the installer, making per-capsule lockfile caching unlikely
to work in the batched branch where rootDir is capsulesDir.

scopes/component/isolator/isolator.main.runtime.ts[782-799]
scopes/component/isolator/isolator.main.runtime.ts[812-827]
scopes/component/isolator/isolator.main.runtime.ts[488-529]
scopes/dependencies/pnpm/lynx.ts[292-353]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `useNesting` is enabled and more than one capsule is installed, the code now uses the batched install path. This path skips the existing per-capsule `cacheLockFileOnly` lockfile-copy optimization, while the cache-on-exit logic still expects `pnpm-lock.yaml` inside each capsule directory.
### Issue Context
The lockfile caching flow (`cacheLockFileOnly`) copies/moves `pnpm-lock.yaml` files per capsule directory. The new batching path installs with `rootDir = capsulesDir`, while pnpm lockfile operations are keyed by the single `rootDir`, so the per-capsule lockfile cache mechanism may not be used for multi-capsule nesting installs.
### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[778-833]
- scopes/component/isolator/isolator.main.runtime.ts[488-529]
### Suggested fix
Choose one consistent strategy for `cacheLockFileOnly` when batching nesting installs:
- Either (A) implement copying/caching of the *root* lockfile (e.g. `${capsulesDir}/pnpm-lock.yaml`) when running the batched install, and update the cache-move code to handle that root lockfile, or
- (B) if per-capsule lockfiles are still required for caching, ensure the batched nesting install produces/uses them (or keep per-capsule installs when `cacheLockFileOnly` is enabled).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 5205789


🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)


Action required
1. Empty nesting triggers install 🐞 Bug ➹ Performance
Description
When useNesting is enabled and skipIfExists filters out all capsules, capsuleList can become empty
but the new condition only special-cases length===1; length===0 now falls into the batched path and
still calls installInCapsules(). This can cause an unnecessary package-manager install in the
capsules root, adding latency and potentially mutating root lockfiles/node_modules even though
nothing needed installation.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
        await Promise.all(
          capsuleList.map(async (capsule, index) => {
            const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
skipIfExists with nesting can reduce capsuleList to an empty list, but the new nesting condition
only handles the single-capsule case. The else branch still invokes installInCapsules(...),
which unconditionally calls into the dependency installer and then the package manager install, so
an empty capsule list still triggers a full install.

scopes/component/isolator/isolator.main.runtime.ts[737-753]
scopes/component/isolator/isolator.main.runtime.ts[769-833]
scopes/component/isolator/isolator.main.runtime.ts[944-1000]
scopes/dependencies/dependency-resolver/dependency-installer.ts[124-153]
scopes/dependencies/dependency-resolver/dependency-installer.ts[273-281]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
With `useNesting` enabled, the code now only uses the per-capsule nesting path when `capsuleList.length === 1`. If `capsuleList.length === 0` (e.g. `skipIfExists` filtered out all capsules), execution falls into the `else` branch and still runs `installInCapsules(...)`, triggering a package-manager install even though there are no capsules to install.

### Issue Context
This is a behavioral change introduced by the new `capsuleList.length === 1` condition. Previously, the nesting branch would no-op on an empty list (because it mapped over capsules), avoiding an expensive install.

### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[737-833]

### Suggested fix
Add an explicit guard to skip installation when there are no capsules to install, e.g.:
- If `installOptions.useNesting && capsuleList.length === 0`: skip `linkInCapsules(...)` / `installInCapsules(...)` and proceed.
- Keep existing behavior for `capsuleList.length === 1` (per-capsule install) and `> 1` (batched install).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Lockfile cache bypassed 🐞 Bug ☼ Reliability
Description
The cacheLockFileOnly lockfile reuse/caching logic is now only executed in the single-capsule
nesting path; multi-capsule nesting installs take the batched branch and skip the per-capsule
lockfile copy. The process-exit cache flow still looks for per-capsule pnpm-lock.yaml files, but the
batched pnpm flow is keyed by a single rootDir, so lockfile-only caching may become
ineffective/no-op for multi-capsule nesting installs.
Code

scopes/component/isolator/isolator.main.runtime.ts[R778-781]

+      if (installOptions.useNesting && capsuleList.length === 1) {
        await Promise.all(
          capsuleList.map(async (capsule, index) => {
            const newCapsuleList = CapsuleList.fromArray([capsule]);
Evidence
The per-capsule lockfile copy is inside the single-capsule nesting branch only. The cache-on-exit
path moves pnpm-lock.yaml from each capsule directory, but pnpm’s install/lockfile bookkeeping is
driven by the single rootDir passed to the installer, making per-capsule lockfile caching unlikely
to work in the batched branch where rootDir is capsulesDir.

scopes/component/isolator/isolator.main.runtime.ts[782-799]
scopes/component/isolator/isolator.main.runtime.ts[812-827]
scopes/component/isolator/isolator.main.runtime.ts[488-529]
scopes/dependencies/pnpm/lynx.ts[292-353]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When `useNesting` is enabled and more than one capsule is installed, the code now uses the batched install path. This path skips the existing per-capsule `cacheLockFileOnly` lockfile-copy optimization, while the cache-on-exit logic still expects `pnpm-lock.yaml` inside each capsule directory.

### Issue Context
The lockfile caching flow (`cacheLockFileOnly`) copies/moves `pnpm-lock.yaml` files per capsule directory. The new batching path installs with `rootDir = capsulesDir`, while pnpm lockfile operations are keyed by the single `rootDir`, so the per-capsule lockfile cache mechanism may not be used for multi-capsule nesting installs.

### Fix Focus Areas
- scopes/component/isolator/isolator.main.runtime.ts[778-833]
- scopes/component/isolator/isolator.main.runtime.ts[488-529]

### Suggested fix
Choose one consistent strategy for `cacheLockFileOnly` when batching nesting installs:
- Either (A) implement copying/caching of the *root* lockfile (e.g. `${capsulesDir}/pnpm-lock.yaml`) when running the batched install, and update the cache-move code to handle that root lockfile, or
- (B) if per-capsule lockfiles are still required for caching, ensure the batched nesting install produces/uses them (or keep per-capsule installs when `cacheLockFileOnly` is enabled).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread scopes/component/isolator/isolator.main.runtime.ts Outdated
…capsule-install

# Conflicts:
#	scopes/component/isolator/isolator.main.runtime.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit dfd76c7

Batching multiple scope-aspect capsules into a single install breaks the
per-capsule dependency isolation the e2e suite enforces (deps-in-capsules,
root-components). Direct-dep placement is fixable via pnpm hoistPattern/
dedupeDirectDeps, but per-capsule peer/version isolation (two versions of the
same aspect needing different peers) leaks in a shared install. Per-capsule
nesting is required for correctness, so revert to master's behavior.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3dba919

…at once

Each scope-aspect/env capsule is installed in its own package-manager install,
previously all fired at once via Promise.all. These installs serialize on the
pnpm store lock, so full concurrency mostly thrashes (CPU oversubscription +
lock contention) and is actually slower than running them one at a time. Cap
the per-capsule install concurrency (1 = sequential): faster wall-clock and far
less CPU, with isolation unchanged.
@davidfirst davidfirst changed the title fix(isolator): batch-install capsules instead of one install per capsule perf(isolator): run per-capsule installs sequentially instead of all at once Jun 27, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 262ff2c

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.

2 participants