From a3c7fea5126ecfb3a8c343cf139437129ef8dea1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 11 May 2026 13:57:33 +0100 Subject: [PATCH] fix(agents): avoid Pi resource discovery stalls Co-authored-by: dataCenter430 --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/compact.ts | 4 +- .../resource-loader.test.ts | 40 +++++++++++++++++++ .../pi-embedded-runner/resource-loader.ts | 23 +++++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 9 ++--- 5 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 src/agents/pi-embedded-runner/resource-loader.test.ts create mode 100644 src/agents/pi-embedded-runner/resource-loader.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 775088a1301..7b698be3241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai - Gateway: share serialized streaming event envelopes across eligible WebSocket and node subscribers while preserving per-client sequence numbers. (#80299) Thanks @samzong. - Browser: report Chrome MCP existing-session page readiness in browser status without letting status probes exceed the client timeout. Fixes #80268. (#80280) Thanks @ai-hpc. - WhatsApp: route opening-phase Baileys 428 connectionClosed through the WhatsApp reconnect policy and keep post-open 428 closes retryable, so transient setup socket closes retry with WhatsApp diagnostics instead of escaping as a bare `channel exited` error. Fixes #75736; mitigates #77443. Thanks @dataCenter430. +- Agents: disable Pi's default filesystem resource discovery for embedded runs while keeping OpenClaw inline extension factories active, avoiding Windows event-loop stalls during first WhatsApp-triggered agent startup. Fixes #77443. Thanks @dataCenter430. - Providers/self-hosted: read model-scoped llama.cpp runtime context from `/props.default_generation_settings.n_ctx` while keeping top-level `n_ctx` as a fallback, so session budgeting reflects the loaded context window. Fixes #73664. (#74057) Thanks @brokemac79. - Memory: reject symlinked directory components in configured extra memory paths before reading Markdown files. (#80331) Thanks @samzong. - Sessions/transcripts: replace whole-file `readFile` scans with shared streaming helpers (`streamSessionTranscriptLines` and `streamSessionTranscriptLinesReverse`) for idempotency lookup, latest/tail assistant text reads, delivery-mirror dedupe, and compaction fork loading, so long-running sessions no longer materialize the full transcript in memory. Forward scans use `readline` over a bounded `createReadStream`; reverse scans read bounded chunks from the file end and decode complete JSONL lines newest-first without a fixed tail cap. Synthetic 200 MiB transcript: peak RSS delta drops from +252 MiB to +27 MiB while preserving malformed-line tolerance and idempotency-key return semantics. Fixes #54296. Thanks @jack-stormentswe. diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 36394c6b1a4..9f118a3f312 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -3,7 +3,6 @@ import os from "node:os"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { createAgentSession, - DefaultResourceLoader, estimateTokens, SessionManager, } from "@earendil-works/pi-coding-agent"; @@ -141,6 +140,7 @@ import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-disco import { readPiModelContextTokens } from "./model-context-tokens.js"; import { resolveModelAsync } from "./model.js"; import { sanitizeSessionHistory, validateReplayTurns } from "./replay-history.js"; +import { createEmbeddedPiResourceLoader } from "./resource-loader.js"; import { buildEmbeddedSandboxInfo } from "./sandbox-info.js"; import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js"; import { resolveEmbeddedRunSkillEntries } from "./skills-runtime.js"; @@ -996,7 +996,7 @@ async function compactEmbeddedPiSessionDirectOnce( modelId, model, }); - const resourceLoader = new DefaultResourceLoader({ + const resourceLoader = createEmbeddedPiResourceLoader({ cwd: resolvedWorkspace, agentDir, settingsManager, diff --git a/src/agents/pi-embedded-runner/resource-loader.test.ts b/src/agents/pi-embedded-runner/resource-loader.test.ts new file mode 100644 index 00000000000..242596b9524 --- /dev/null +++ b/src/agents/pi-embedded-runner/resource-loader.test.ts @@ -0,0 +1,40 @@ +import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; +import { describe, expect, it, vi } from "vitest"; +import { + createEmbeddedPiResourceLoader, + EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS, +} from "./resource-loader.js"; + +vi.mock("@earendil-works/pi-coding-agent", () => ({ + DefaultResourceLoader: vi.fn(function DefaultResourceLoader( + this: Record, + options: unknown, + ) { + Object.assign(this, { + options, + reload: vi.fn(async () => undefined), + }); + }), +})); + +describe("createEmbeddedPiResourceLoader", () => { + it("keeps inline extensions but disables Pi filesystem discovery", () => { + const settingsManager = {}; + const extensionFactories = [vi.fn()]; + + createEmbeddedPiResourceLoader({ + cwd: "/workspace", + agentDir: "/agent", + settingsManager: settingsManager as never, + extensionFactories: extensionFactories as never, + }); + + expect(DefaultResourceLoader).toHaveBeenCalledWith({ + cwd: "/workspace", + agentDir: "/agent", + settingsManager, + extensionFactories, + ...EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS, + }); + }); +}); diff --git a/src/agents/pi-embedded-runner/resource-loader.ts b/src/agents/pi-embedded-runner/resource-loader.ts new file mode 100644 index 00000000000..0f122d21792 --- /dev/null +++ b/src/agents/pi-embedded-runner/resource-loader.ts @@ -0,0 +1,23 @@ +import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; + +type DefaultResourceLoaderInit = ConstructorParameters[0]; + +export const EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS = { + noExtensions: true, + noSkills: true, + noPromptTemplates: true, + noThemes: true, + noContextFiles: true, +} satisfies Partial; + +export function createEmbeddedPiResourceLoader( + options: Pick< + DefaultResourceLoaderInit, + "cwd" | "agentDir" | "settingsManager" | "extensionFactories" + >, +): DefaultResourceLoader { + return new DefaultResourceLoader({ + ...options, + ...EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS, + }); +} diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 862170f7225..8b80f12f1ce 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -2,11 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { - createAgentSession, - DefaultResourceLoader, - SessionManager, -} from "@earendil-works/pi-coding-agent"; +import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; import { isAcpRuntimeSpawnAvailable } from "../../../acp/runtime/availability.js"; import { buildHierarchyReinforcementMessage } from "../../../auto-reply/handoff-summarizer.js"; import { filterHeartbeatPairs } from "../../../auto-reply/heartbeat-filter.js"; @@ -223,6 +219,7 @@ import { validateReplayTurns, } from "../replay-history.js"; import { observeReplayMetadata, replayMetadataFromState } from "../replay-state.js"; +import { createEmbeddedPiResourceLoader } from "../resource-loader.js"; import { clearActiveEmbeddedRun, type EmbeddedPiQueueHandle, @@ -1715,7 +1712,7 @@ export async function runEmbeddedAttempt( modelId: params.modelId, model: params.model, }); - const resourceLoader = new DefaultResourceLoader({ + const resourceLoader = createEmbeddedPiResourceLoader({ cwd: resolvedWorkspace, agentDir, settingsManager,