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.
This commit is contained in:
Peter Steinberger
2026-05-04 23:13:32 +01:00
committed by GitHub
parent 343f859b90
commit a7263de258
7 changed files with 42 additions and 10 deletions

View File

@@ -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 `<rootDir>/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 <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.

View File

@@ -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 () => {

View File

@@ -216,6 +216,7 @@ async function resolveRuntimeModel(params: {
provider: string;
model: string;
agentDir: string;
workspaceDir?: string;
sessionEntry?: StoredSessionEntry;
sessionStore?: Record<string, StoredSessionEntry>;
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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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" }],

View File

@@ -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,