fix: compaction safeguard extension not loading in production builds (openclaw#22349) thanks @Glucksberg

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini (local run had unrelated baseline failures; Tak approved proceed)

Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Glucksberg
2026-02-20 23:21:09 -04:00
committed by GitHub
parent e2dbd45418
commit 1410d15c5e
4 changed files with 59 additions and 34 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
### Fixes ### Fixes
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
- Auto-reply/Tools: forward `senderIsOwner` through embedded queued/followup runner params so owner-only tools remain available for authorized senders. (#22296) thanks @hcoj. - Auto-reply/Tools: forward `senderIsOwner` through embedded queued/followup runner params so owner-only tools remain available for authorized senders. (#22296) thanks @hcoj.
- Agents/Subagents: restore announce-chain delivery to agent injection, defer nested announce output until descendant follow-up content is ready, and prevent descendant deferrals from consuming announce retry budget so deep chains do not drop final completions. (#22223) Thanks @tyler6204. - Agents/Subagents: restore announce-chain delivery to agent injection, defer nested announce output until descendant follow-up content is ready, and prevent descendant deferrals from consuming announce retry budget so deep chains do not drop final completions. (#22223) Thanks @tyler6204.
- Gateway/Auth: require `gateway.trustedProxies` to include a loopback proxy address when `auth.mode="trusted-proxy"` and `bind="loopback"`, preventing same-host proxy misconfiguration from silently blocking auth. (#22082, follow-up to #20097) thanks @mbelinky. - Gateway/Auth: require `gateway.trustedProxies` to include a loopback proxy address when `auth.mode="trusted-proxy"` and `bind="loopback"`, preventing same-host proxy misconfiguration from silently blocking auth. (#22082, follow-up to #20097) thanks @mbelinky.

View File

@@ -3,6 +3,7 @@ import os from "node:os";
import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { import {
createAgentSession, createAgentSession,
DefaultResourceLoader,
estimateTokens, estimateTokens,
SessionManager, SessionManager,
SettingsManager, SettingsManager,
@@ -60,7 +61,7 @@ import {
compactWithSafetyTimeout, compactWithSafetyTimeout,
EMBEDDED_COMPACTION_TIMEOUT_MS, EMBEDDED_COMPACTION_TIMEOUT_MS,
} from "./compaction-safety-timeout.js"; } from "./compaction-safety-timeout.js";
import { buildEmbeddedExtensionPaths } from "./extensions.js"; import { buildEmbeddedExtensionFactories } from "./extensions.js";
import { import {
logToolSchemasForGoogle, logToolSchemasForGoogle,
sanitizeSessionHistory, sanitizeSessionHistory,
@@ -533,14 +534,27 @@ export async function compactEmbeddedPiSessionDirect(
settingsManager, settingsManager,
cfg: params.config, cfg: params.config,
}); });
// Call for side effects (sets compaction/pruning runtime state) // Sets compaction/pruning runtime state and returns extension factories
buildEmbeddedExtensionPaths({ // that must be passed to the resource loader for the safeguard to be active.
const extensionFactories = buildEmbeddedExtensionFactories({
cfg: params.config, cfg: params.config,
sessionManager, sessionManager,
provider, provider,
modelId, modelId,
model, model,
}); });
// Only create an explicit resource loader when there are extension factories
// to register; otherwise let createAgentSession use its built-in default.
let resourceLoader: DefaultResourceLoader | undefined;
if (extensionFactories.length > 0) {
resourceLoader = new DefaultResourceLoader({
cwd: resolvedWorkspace,
agentDir,
settingsManager,
extensionFactories,
});
await resourceLoader.reload();
}
const { builtInTools, customTools } = splitSdkTools({ const { builtInTools, customTools } = splitSdkTools({
tools, tools,
@@ -558,6 +572,7 @@ export async function compactEmbeddedPiSessionDirect(
customTools, customTools,
sessionManager, sessionManager,
settingsManager, settingsManager,
resourceLoader,
}); });
applySystemPromptOverrideToSession(session, systemPromptOverride()); applySystemPromptOverrideToSession(session, systemPromptOverride());

View File

@@ -1,25 +1,17 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { Api, Model } from "@mariozechner/pi-ai"; import type { Api, Model } from "@mariozechner/pi-ai";
import type { SessionManager } from "@mariozechner/pi-coding-agent"; import type { ExtensionFactory, SessionManager } from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { resolveContextWindowInfo } from "../context-window-guard.js"; import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { setCompactionSafeguardRuntime } from "../pi-extensions/compaction-safeguard-runtime.js"; import { setCompactionSafeguardRuntime } from "../pi-extensions/compaction-safeguard-runtime.js";
import compactionSafeguardExtension from "../pi-extensions/compaction-safeguard.js";
import contextPruningExtension from "../pi-extensions/context-pruning.js";
import { setContextPruningRuntime } from "../pi-extensions/context-pruning/runtime.js"; import { setContextPruningRuntime } from "../pi-extensions/context-pruning/runtime.js";
import { computeEffectiveSettings } from "../pi-extensions/context-pruning/settings.js"; import { computeEffectiveSettings } from "../pi-extensions/context-pruning/settings.js";
import { makeToolPrunablePredicate } from "../pi-extensions/context-pruning/tools.js"; import { makeToolPrunablePredicate } from "../pi-extensions/context-pruning/tools.js";
import { ensurePiCompactionReserveTokens } from "../pi-settings.js"; import { ensurePiCompactionReserveTokens } from "../pi-settings.js";
import { isCacheTtlEligibleProvider, readLastCacheTtlTimestamp } from "./cache-ttl.js"; import { isCacheTtlEligibleProvider, readLastCacheTtlTimestamp } from "./cache-ttl.js";
function resolvePiExtensionPath(id: string): string {
const self = fileURLToPath(import.meta.url);
const dir = path.dirname(self);
// In dev this file is `.ts` (tsx), in production it's `.js`.
const ext = path.extname(self) === ".ts" ? "ts" : "js";
return path.join(dir, "..", "pi-extensions", `${id}.${ext}`);
}
function resolveContextWindowTokens(params: { function resolveContextWindowTokens(params: {
cfg: OpenClawConfig | undefined; cfg: OpenClawConfig | undefined;
provider: string; provider: string;
@@ -35,24 +27,24 @@ function resolveContextWindowTokens(params: {
}).tokens; }).tokens;
} }
function buildContextPruningExtension(params: { function buildContextPruningFactory(params: {
cfg: OpenClawConfig | undefined; cfg: OpenClawConfig | undefined;
sessionManager: SessionManager; sessionManager: SessionManager;
provider: string; provider: string;
modelId: string; modelId: string;
model: Model<Api> | undefined; model: Model<Api> | undefined;
}): { additionalExtensionPaths?: string[] } { }): ExtensionFactory | undefined {
const raw = params.cfg?.agents?.defaults?.contextPruning; const raw = params.cfg?.agents?.defaults?.contextPruning;
if (raw?.mode !== "cache-ttl") { if (raw?.mode !== "cache-ttl") {
return {}; return undefined;
} }
if (!isCacheTtlEligibleProvider(params.provider, params.modelId)) { if (!isCacheTtlEligibleProvider(params.provider, params.modelId)) {
return {}; return undefined;
} }
const settings = computeEffectiveSettings(raw); const settings = computeEffectiveSettings(raw);
if (!settings) { if (!settings) {
return {}; return undefined;
} }
setContextPruningRuntime(params.sessionManager, { setContextPruningRuntime(params.sessionManager, {
@@ -62,23 +54,21 @@ function buildContextPruningExtension(params: {
lastCacheTouchAt: readLastCacheTtlTimestamp(params.sessionManager), lastCacheTouchAt: readLastCacheTtlTimestamp(params.sessionManager),
}); });
return { return contextPruningExtension;
additionalExtensionPaths: [resolvePiExtensionPath("context-pruning")],
};
} }
function resolveCompactionMode(cfg?: OpenClawConfig): "default" | "safeguard" { function resolveCompactionMode(cfg?: OpenClawConfig): "default" | "safeguard" {
return cfg?.agents?.defaults?.compaction?.mode === "safeguard" ? "safeguard" : "default"; return cfg?.agents?.defaults?.compaction?.mode === "safeguard" ? "safeguard" : "default";
} }
export function buildEmbeddedExtensionPaths(params: { export function buildEmbeddedExtensionFactories(params: {
cfg: OpenClawConfig | undefined; cfg: OpenClawConfig | undefined;
sessionManager: SessionManager; sessionManager: SessionManager;
provider: string; provider: string;
modelId: string; modelId: string;
model: Model<Api> | undefined; model: Model<Api> | undefined;
}): string[] { }): ExtensionFactory[] {
const paths: string[] = []; const factories: ExtensionFactory[] = [];
if (resolveCompactionMode(params.cfg) === "safeguard") { if (resolveCompactionMode(params.cfg) === "safeguard") {
const compactionCfg = params.cfg?.agents?.defaults?.compaction; const compactionCfg = params.cfg?.agents?.defaults?.compaction;
const contextWindowInfo = resolveContextWindowInfo({ const contextWindowInfo = resolveContextWindowInfo({
@@ -92,13 +82,13 @@ export function buildEmbeddedExtensionPaths(params: {
maxHistoryShare: compactionCfg?.maxHistoryShare, maxHistoryShare: compactionCfg?.maxHistoryShare,
contextWindowTokens: contextWindowInfo.tokens, contextWindowTokens: contextWindowInfo.tokens,
}); });
paths.push(resolvePiExtensionPath("compaction-safeguard")); factories.push(compactionSafeguardExtension);
} }
const pruning = buildContextPruningExtension(params); const pruningFactory = buildContextPruningFactory(params);
if (pruning.additionalExtensionPaths) { if (pruningFactory) {
paths.push(...pruning.additionalExtensionPaths); factories.push(pruningFactory);
} }
return paths; return factories;
} }
export { ensurePiCompactionReserveTokens }; export { ensurePiCompactionReserveTokens };

View File

@@ -3,7 +3,12 @@ import os from "node:os";
import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent } from "@mariozechner/pi-ai"; import type { ImageContent } from "@mariozechner/pi-ai";
import { streamSimple } from "@mariozechner/pi-ai"; import { streamSimple } from "@mariozechner/pi-ai";
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent"; import {
createAgentSession,
DefaultResourceLoader,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js"; import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js"; import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
import { getMachineDisplayName } from "../../../infra/machine-name.js"; import { getMachineDisplayName } from "../../../infra/machine-name.js";
@@ -70,7 +75,7 @@ import { resolveTranscriptPolicy } from "../../transcript-policy.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js"; import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
import { isRunnerAbortError } from "../abort.js"; import { isRunnerAbortError } from "../abort.js";
import { appendCacheTtlTimestamp, isCacheTtlEligibleProvider } from "../cache-ttl.js"; import { appendCacheTtlTimestamp, isCacheTtlEligibleProvider } from "../cache-ttl.js";
import { buildEmbeddedExtensionPaths } from "../extensions.js"; import { buildEmbeddedExtensionFactories } from "../extensions.js";
import { applyExtraParamsToAgent } from "../extra-params.js"; import { applyExtraParamsToAgent } from "../extra-params.js";
import { import {
logToolSchemasForGoogle, logToolSchemasForGoogle,
@@ -534,14 +539,27 @@ export async function runEmbeddedAttempt(
cfg: params.config, cfg: params.config,
}); });
// Call for side effects (sets compaction/pruning runtime state) // Sets compaction/pruning runtime state and returns extension factories
buildEmbeddedExtensionPaths({ // that must be passed to the resource loader for the safeguard to be active.
const extensionFactories = buildEmbeddedExtensionFactories({
cfg: params.config, cfg: params.config,
sessionManager, sessionManager,
provider: params.provider, provider: params.provider,
modelId: params.modelId, modelId: params.modelId,
model: params.model, model: params.model,
}); });
// Only create an explicit resource loader when there are extension factories
// to register; otherwise let createAgentSession use its built-in default.
let resourceLoader: DefaultResourceLoader | undefined;
if (extensionFactories.length > 0) {
resourceLoader = new DefaultResourceLoader({
cwd: resolvedWorkspace,
agentDir,
settingsManager,
extensionFactories,
});
await resourceLoader.reload();
}
// Get hook runner early so it's available when creating tools // Get hook runner early so it's available when creating tools
const hookRunner = getGlobalHookRunner(); const hookRunner = getGlobalHookRunner();
@@ -584,6 +602,7 @@ export async function runEmbeddedAttempt(
customTools: allCustomTools, customTools: allCustomTools,
sessionManager, sessionManager,
settingsManager, settingsManager,
resourceLoader,
})); }));
applySystemPromptOverrideToSession(session, systemPromptText); applySystemPromptOverrideToSession(session, systemPromptText);
if (!session) { if (!session) {