From a7263de2584e1e9434e707843b815627649f53af Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 23:13:32 +0100 Subject: [PATCH] fix(agents): preserve workspace metadata reuse Pass the resolved agent workspace through hot model refresh paths so workspace-scoped plugin metadata snapshots can be reused. Refs #77519. Refs #77532. --- CHANGELOG.md | 1 + src/agents/btw.test.ts | 7 +++++++ src/agents/btw.ts | 15 +++++++++------ src/agents/pi-embedded-runner/compact.ts | 4 +++- src/agents/pi-embedded-runner/run.ts | 4 +++- src/agents/tools/pdf-tool.test.ts | 16 +++++++++++++++- src/agents/tools/pdf-tool.ts | 5 ++++- 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47512b23ff9..c0688104e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Changes - Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure. +- Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532) - Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys. - Secrets/external channel contracts: also look in `/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss. - Models/auth: add `openclaw models auth list [--provider ] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc. diff --git a/src/agents/btw.test.ts b/src/agents/btw.test.ts index e83b3f179d3..54feec553e3 100644 --- a/src/agents/btw.test.ts +++ b/src/agents/btw.test.ts @@ -417,6 +417,13 @@ describe("runBtwSideQuestion", () => { const result = await runSideQuestion(); expect(result).toEqual({ text: "Final answer." }); + expect(ensureOpenClawModelsJsonMock).toHaveBeenCalledWith( + expect.any(Object), + DEFAULT_AGENT_DIR, + { + workspaceDir: "/tmp/workspace", + }, + ); }); it("applies provider runtime auth before streaming github-copilot BTW questions", async () => { diff --git a/src/agents/btw.ts b/src/agents/btw.ts index 43bb5e0f444..4f8512ae3b5 100644 --- a/src/agents/btw.ts +++ b/src/agents/btw.ts @@ -216,6 +216,7 @@ async function resolveRuntimeModel(params: { provider: string; model: string; agentDir: string; + workspaceDir?: string; sessionEntry?: StoredSessionEntry; sessionStore?: Record; sessionKey?: string; @@ -226,7 +227,8 @@ async function resolveRuntimeModel(params: { authProfileId?: string; authProfileIdSource?: "auto" | "user"; }> { - await ensureOpenClawModelsJson(params.cfg, params.agentDir); + const modelsOptions = params.workspaceDir ? { workspaceDir: params.workspaceDir } : undefined; + await ensureOpenClawModelsJson(params.cfg, params.agentDir, modelsOptions); const authStorage = discoverAuthStorage(params.agentDir); const modelRegistry = discoverModels(authStorage, params.agentDir); const model = resolveModelWithRegistry({ @@ -319,11 +321,17 @@ export async function runBtwSideQuestion( throw new Error("No active session context."); } + const sessionAgentId = resolveSessionAgentId({ + sessionKey: params.sessionKey, + config: params.cfg, + }); + const workspaceDir = resolveAgentWorkspaceDir(params.cfg, sessionAgentId); const { model, authProfileId } = await resolveRuntimeModel({ cfg: params.cfg, provider: params.provider, model: params.model, agentDir: params.agentDir, + workspaceDir, sessionEntry: params.sessionEntry, sessionStore: params.sessionStore, sessionKey: params.sessionKey, @@ -341,11 +349,6 @@ export async function runBtwSideQuestion( apiKeyInfo.mode === "aws-sdk" && !apiKeyInfo.apiKey ? undefined : requireApiKey(apiKeyInfo, model.provider); - const sessionAgentId = resolveSessionAgentId({ - sessionKey: params.sessionKey, - config: params.cfg, - }); - const workspaceDir = resolveAgentWorkspaceDir(params.cfg, sessionAgentId); if (apiKey) { const preparedAuth = await prepareProviderRuntimeAuth({ provider: model.provider, diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 2c63232c19c..9917ddc9361 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -482,7 +482,9 @@ async function compactEmbeddedPiSessionDirectOnce( }; }; const agentDir = params.agentDir ?? resolveOpenClawAgentDir(); - await ensureOpenClawModelsJson(params.config, agentDir); + await ensureOpenClawModelsJson(params.config, agentDir, { + workspaceDir: resolvedWorkspace, + }); const { model, error, authStorage, modelRegistry } = await resolveModelAsync( provider, modelId, diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 5fc93d36a66..8e7126bf32d 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -503,7 +503,9 @@ export async function runEmbeddedPiAgent( dynamicModelResolution.model || pluginHarnessOwnsTransport ? dynamicModelResolution : await (async () => { - await ensureOpenClawModelsJson(params.config, agentDir); + await ensureOpenClawModelsJson(params.config, agentDir, { + workspaceDir: resolvedWorkspace, + }); return await resolveModelAsync(provider, modelId, agentDir, params.config); })(); const { model, error, authStorage, modelRegistry } = modelResolution; diff --git a/src/agents/tools/pdf-tool.test.ts b/src/agents/tools/pdf-tool.test.ts index eee305d0186..77a30edd71e 100644 --- a/src/agents/tools/pdf-tool.test.ts +++ b/src/agents/tools/pdf-tool.test.ts @@ -394,17 +394,31 @@ describe("createPdfTool", () => { it("uses native PDF path without eager extraction", async () => { await withTempPdfAgentDir(async (agentDir) => { + const workspaceDir = path.join(agentDir, "workspace"); await stubPdfToolInfra(agentDir, { provider: "anthropic", input: ["text", "document"] }); vi.spyOn(pdfNativeProviders, "anthropicAnalyzePdf").mockResolvedValue("native summary"); const extractSpy = vi.spyOn(pdfExtractModule, "extractPdfContent"); const cfg = withPdfModel(ANTHROPIC_PDF_MODEL); - const tool = requirePdfTool((await loadCreatePdfTool())({ config: cfg, agentDir })); + const tool = requirePdfTool( + (await loadCreatePdfTool())({ config: cfg, agentDir, workspaceDir }), + ); const result = await tool.execute("t1", { prompt: "summarize", pdf: "/tmp/doc.pdf", }); + expect(modelsConfig.ensureOpenClawModelsJson).toHaveBeenCalledWith( + expect.objectContaining({ + agents: expect.objectContaining({ + defaults: expect.objectContaining({ + pdfModel: { primary: ANTHROPIC_PDF_MODEL }, + }), + }), + }), + agentDir, + { workspaceDir }, + ); expect(extractSpy).not.toHaveBeenCalled(); expect(result).toMatchObject({ content: [{ type: "text", text: "native summary" }], diff --git a/src/agents/tools/pdf-tool.ts b/src/agents/tools/pdf-tool.ts index e848f9f7e83..65fbac9af24 100644 --- a/src/agents/tools/pdf-tool.ts +++ b/src/agents/tools/pdf-tool.ts @@ -127,6 +127,7 @@ type PdfSandboxConfig = { async function runPdfPrompt(params: { cfg?: OpenClawConfig; agentDir: string; + workspaceDir?: string; pdfModelConfig: ImageModelConfig; modelOverride?: string; prompt: string; @@ -142,7 +143,8 @@ async function runPdfPrompt(params: { }> { const effectiveCfg = applyImageModelConfigDefaults(params.cfg, params.pdfModelConfig); - await ensureOpenClawModelsJson(effectiveCfg, params.agentDir); + const modelsOptions = params.workspaceDir ? { workspaceDir: params.workspaceDir } : undefined; + await ensureOpenClawModelsJson(effectiveCfg, params.agentDir, modelsOptions); const authStorage = discoverAuthStorage(params.agentDir); const modelRegistry = discoverModels(authStorage, params.agentDir); @@ -482,6 +484,7 @@ export function createPdfTool(options?: { const result = await runPdfPrompt({ cfg: options?.config, agentDir, + ...(options?.workspaceDir ? { workspaceDir: options.workspaceDir } : {}), pdfModelConfig, modelOverride, prompt: promptRaw,