Skip to content

perf: skip packageManager.resolve() by loading extensionFactories directly#82536

Closed
sunvember wants to merge 2 commits into
openclaw:mainfrom
sunvember:perf/skip-resolve-direct-load-extensions
Closed

perf: skip packageManager.resolve() by loading extensionFactories directly#82536
sunvember wants to merge 2 commits into
openclaw:mainfrom
sunvember:perf/skip-resolve-direct-load-extensions

Conversation

@sunvember

@sunvember sunvember commented May 16, 2026

Copy link
Copy Markdown

Summary

This PR eliminates the 5-9 second packageManager.resolve() overhead on every embedded run by skipping reload() and loading inline extensionFactories directly.

Problem

The previous approach (PR #82523, now closed) attempted to cache DefaultResourceLoader instances, but was fundamentally flawed:

  1. Bottleneck misdiagnosis: The overhead is NOT in the constructor (which only assigns properties in milliseconds), but in reload()packageManager.resolve()
  2. Caching ineffective: Callers always invoke await loader.reload(), so caching doesn't avoid the resolve() overhead
  3. Stale closure bug: Cached loader's extensionFactories closure captured stale values from the first run

Root Cause Analysis

packageManager.resolve() scans:

  • ~/.pi/extensions/skills/prompts/themes
  • <cwd>/.pi/extensions/skills/prompts/themes
  • ~/.agents/skills
  • All ancestor .agents/skills directories up to git repo root

When OpenClaw's embedded runs use all no* flags true (noExtensions, noSkills, noPromptTemplates, noThemes, noContextFiles):

  • resolve() executes all filesystem scans (expensive, 5-9 seconds)
  • Results are discarded → empty arrays for skills/extensions/prompts/themes
  • Only loadExtensionFactories() produces useful output (inline extensions for compaction safeguards)

Key insight: The DefaultResourceLoader constructor already initializes the state that matches reload() output when no* flags are true:

  • extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() }
  • skills = [], prompts = [], themes = [], agentsFiles = []

The only difference is systemPrompt = undefined, but OpenClaw overrides this via applySystemPromptOverrideToSession() anyway.

Solution

Instead of caching loaders, we:

  1. Create a new loader with no* flags true (milliseconds)
  2. Call loadExtensionFactories(runtime) directly (public method, async but fast)
  3. Skip reload() entirely
// Before (5-9s overhead)
const loader = new DefaultResourceLoader({...});
await loader.reload(); // ← resolve() scans many directories

// After (~milliseconds)
const loader = new DefaultResourceLoader({...});
const runtime = loader.extensionsResult.runtime;
const inline = await loader.loadExtensionFactories(runtime); // ← direct call
loader.extensionsResult.extensions.push(...inline.extensions);

Changes

File Change
resource-loader.ts createEmbeddedPiResourceLoader is now async, loads extensionFactories directly, no caching
attempt.ts await createEmbeddedPiResourceLoader() + removed await loader.reload()
compact.ts Same pattern
resource-loader.test.ts Rewritten for new behavior

Performance Impact

  • Before: 5-9 seconds overhead per embedded run (filesystem scans)
  • After: ~milliseconds (only async extension factory loading)

Safety

  • systemPrompt being undefined is safe because OpenClaw overrides it via applySystemPromptOverrideToSession()
  • Compaction guards (applyPiCompactionSettingsFromConfig, applyPiAutoCompactionGuard) still execute after loader creation
  • Inline extensions for compaction safeguards are loaded correctly via loadExtensionFactories()

Related

sunvember added 2 commits May 16, 2026 17:18
…oad overhead

DefaultResourceLoader construction triggers packageManager.resolve() which
scans skills directories and blocks the event loop for 5-9 seconds on every
embedded run. This is pure overhead when the same workspace/agent is reused
across consecutive runs (normal session flow).

Solution:
- Add in-memory cache keyed by (cwd, agentDir) in resource-loader.ts
- Cache TTL defaults to 60s, configurable via OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS
- add markResourceLoaderReloaded() to track reload completion
- Add pruneResourceLoaderCache() for periodic cleanup
- Add invalidateResourceLoaderCache() for targeted or full cache invalidation
- Call markResourceLoaderReloaded in attempt.ts and compact.ts after reload()

Files changed:
- resource-loader.ts: cache logic
- resource-loader.test.ts: 10 new test cases
- attempt.ts: call markResourceLoaderReloaded after reload
- compact.ts: call markResourceLoaderReloaded after reload

AI-assisted development
…ectly

## Problem

The previous PR (openclaw#82523) attempted to cache DefaultResourceLoader instances,
but this approach was fundamentally flawed because:

1. The bottleneck is NOT in the constructor (which only assigns properties)
2. The bottleneck is in `reload()` → `packageManager.resolve()`
3. `resolve()` scans many directories (~/.pi/*, ~/.agents/skills, ancestor
   .agents/skills up to git repo root)
4. Caching the loader doesn't help because callers always call `await loader.reload()`
5. The cached loader's extensionFactories closure captured stale values

## Root Cause Analysis

When all `no*` flags are true:
- `resolve()` still executes all filesystem scans (expensive)
- But the results are discarded (empty arrays for skills/extensions/prompts/themes)
- Only `loadExtensionFactories()` produces useful output

The `DefaultResourceLoader` constructor initializes:
- `extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() }`
- `skills = [], prompts = [], themes = [], agentsFiles = []`
- `systemPrompt = undefined`

These match `reload()` output when `no*` flags are true, except `systemPrompt`.
OpenClaw overrides `systemPrompt` via `applySystemPromptOverrideToSession()`,
so the undefined value is harmless.

## Solution

Instead of caching loaders, we:
1. Create a new loader with `no*` flags true
2. Call `loadExtensionFactories()` directly (public method)
3. Skip `reload()` entirely

This eliminates the 5-9 second `resolve()` overhead while still loading
inline extensions needed for compaction safeguards and tool middleware.

## Changes

- `resource-loader.ts`: Changed to async function, loads extensionFactories
  directly without calling reload()
- `attempt.ts`: Use `await createEmbeddedPiResourceLoader()` + remove reload()
- `compact.ts`: Same pattern
- `resource-loader.test.ts`: Rewritten to match new behavior

## Performance Impact

Before: 5-9 seconds overhead per embedded run
After: ~milliseconds (only extension factory loading, no filesystem scans)

Files changed:
- src/agents/pi-embedded-runner/resource-loader.ts
- src/agents/pi-embedded-runner/resource-loader.test.ts
- src/agents/pi-embedded-runner/compact.ts
- src/agents/pi-embedded-runner/run/attempt.ts
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 16, 2026
@clawsweeper

clawsweeper Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution. I reviewed the branch, and this PR is not a good landing base for OpenClaw.

Close: the performance problem is still real, but this branch is no longer a viable landing candidate because it patches the retired Pi runner path, bypasses private loader internals, and still lacks after-fix real behavior proof.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #79899
Summary: This PR is a candidate fix for the canonical DefaultResourceLoader.reload() event-loop stall issue, but the branch is stale and not a safe landing path.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

So I’m closing this PR rather than keeping an unmergeable branch open. A new narrow PR that carries only the useful part is welcome.

Review details

Best possible solution:

Keep #79899 as the canonical work item and replace this stale branch with a fresh current-runner PR that adds a loader-owned fast path with real timing proof.

Do we have a high-confidence way to reproduce the issue?

No, not for the exact Windows or large-tree timing magnitude. Source inspection does show the current and latest-release path still calls resourceLoader.reload() and packageManager.resolve().

Is this the best way to solve the issue?

No. The performance direction is plausible, but this PR patches retired files and bypasses private loader internals instead of adding a supported fast path on the current runner.

Security review:

Security review cleared: The diff changes agent runtime TypeScript and tests only; no dependency, workflow, secret, install, package, or supply-chain surface is changed.

AGENTS.md: found and applied where relevant.

What I checked:

Likely related people:

  • vincentkoc: Current blame for the live embedded-agent resource-loader, attempt reload call, compaction reload call, and session loader implementation points to the recent current-main path refresh commit. (role: recent current-main contributor; confidence: medium; commits: ab165d119cc7; files: src/agents/embedded-agent-runner/resource-loader.ts, src/agents/embedded-agent-runner/run/attempt.ts, src/agents/embedded-agent-runner/compact.ts)
  • steipete: Prior ClawSweeper history in the PR discussion ties the earlier embedded no-discovery loader shape and the Pi-to-embedded-agent rename to this account's commits, making them relevant for owner routing. (role: feature-history owner; confidence: medium; commits: a3c7fea5126e, bb46b79d3c14; files: src/agents/pi-embedded-runner/resource-loader.ts, src/agents/pi-embedded-runner/run/attempt.ts, src/agents/pi-embedded-runner/compact.ts)
  • ly85206559: The related merged compaction PR preserved compaction settings across resource-loader reload, which is one semantic any supported fast path must keep. (role: adjacent reload-preservation contributor; confidence: medium; commits: 55d9b6c2a600, c696718fc46c; files: src/agents/pi-embedded-runner/run/attempt.ts, src/agents/pi-embedded-runner/compact.ts)
  • Alix-007: The merged warm transcript cache PR fixed another session-resource-loader subpath and explicitly left the constant loader reload() baseline cost for the canonical issue/PR thread. (role: adjacent performance fix contributor; confidence: medium; commits: e4473025adb3, a0b16f37e835; files: src/agents/sessions/session-manager.ts, src/agents/embedded-agent-runner/session-manager-init.ts)

Codex review notes: model internal, reasoning high; reviewed against 6c2c43d63f4c.

@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant