fix(agents): reapply compaction settings after resource loader reload (#65602) (#67146)

Merged via squash.

Prepared head SHA: 4978f7b8b5
Co-authored-by: ly85206559 <12526624+ly85206559@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
ly85206559
2026-04-21 06:10:24 +08:00
committed by GitHub
parent d7c7905a52
commit 35cb59e3b5
6 changed files with 117 additions and 26 deletions

View File

@@ -25,7 +25,7 @@ Docs: https://docs.openclaw.ai
- Gateway/device pairing: restrict non-admin paired-device sessions (device-token auth) to their own pairing list, approve, and reject actions so a paired device cannot enumerate other devices or approve/reject pairing requests authored by another device. Admin and shared-secret operator sessions retain full visibility. (#69375) Thanks @eleqtrizit.
- Agents/gateway tool: extend the agent-facing `gateway` tool's config mutation guard so model-driven `config.patch` and `config.apply` cannot rewrite operator-trusted paths (sandbox, plugin trust, gateway auth/TLS, hook routing and tokens, SSRF policy, MCP servers, workspace filesystem hardening) and cannot bypass the guard by editing per-agent sandbox, tools, or embedded-Pi overrides in place under `agents.list[]`. (#69377) Thanks @eleqtrizit.
- Gateway/websocket broadcasts: require `operator.read` (or higher) for chat, agent, and tool-result event frames so pairing-scoped and node-role sessions no longer passively receive session chat content, and scope-gate unknown broadcast events by default. Plugin-defined `plugin.*` broadcasts are scoped to operator.write/admin, and status/transport events (`heartbeat`, `presence`, `tick`, etc.) remain unrestricted. Per-client sequence numbers preserve per-connection monotonicity. (#69373) Thanks @eleqtrizit.
- Agents/compaction: always reload embedded Pi resources through an explicit loader and reapply reserve-token overrides so runs without extension factories no longer silently lose compaction settings before session start. (#67146) Thanks @ly85206559.
## 2026.4.20
### Changes

View File

@@ -70,6 +70,7 @@ import {
consumeCompactionSafeguardCancelReason,
setCompactionSafeguardCancelReason,
} from "../pi-hooks/compaction-safeguard-runtime.js";
import { applyPiCompactionSettingsFromConfig } from "../pi-settings.js";
import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js";
import { createOpenClawCodingTools } from "../pi-tools.js";
import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js";
@@ -823,18 +824,20 @@ export async function compactEmbeddedPiSessionDirect(
modelId,
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 resourceLoader = new DefaultResourceLoader({
cwd: resolvedWorkspace,
agentDir,
settingsManager,
extensionFactories,
});
await resourceLoader.reload();
// DefaultResourceLoader.reload() rehydrates settings from disk and can drop OpenClaw
// compaction overrides applied in createPreparedEmbeddedPiSettingsManager.
applyPiCompactionSettingsFromConfig({
settingsManager,
cfg: params.config,
contextTokenBudget: ctxInfo.tokens,
});
const { builtInTools, customTools } = splitSdkTools({
tools: effectiveTools,

View File

@@ -0,0 +1,43 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
cleanupTempPaths,
createContextEngineAttemptRunner,
getHoisted,
resetEmbeddedAttemptHarness,
} from "./attempt.spawn-workspace.test-support.js";
const hoisted = getHoisted();
describe("runEmbeddedAttempt resource loader wiring", () => {
const tempPaths: string[] = [];
beforeEach(() => {
resetEmbeddedAttemptHarness();
});
afterEach(async () => {
await cleanupTempPaths(tempPaths);
});
it("passes an explicit resourceLoader to createAgentSession even without extension factories", async () => {
await createContextEngineAttemptRunner({
sessionKey: "agent:main:discord:dm:test-resource-loader",
tempPaths,
contextEngine: {
assemble: async ({ messages }) => ({
messages,
estimatedTokens: 1,
}),
},
});
expect(hoisted.createAgentSessionMock).toHaveBeenCalled();
expect(hoisted.createAgentSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
resourceLoader: expect.objectContaining({
reload: expect.any(Function),
}),
}),
);
});
});

View File

@@ -284,6 +284,13 @@ vi.mock("../../pi-project-settings.js", () => ({
vi.mock("../../pi-settings.js", () => ({
applyPiAutoCompactionGuard: () => {},
applyPiCompactionSettingsFromConfig: () => ({
didOverride: false,
compaction: {
reserveTokens: 0,
keepRecentTokens: 40_000,
},
}),
}));
vi.mock("../extensions.js", () => ({

View File

@@ -79,8 +79,11 @@ import {
resolveBootstrapTotalMaxChars,
} from "../../pi-embedded-helpers.js";
import { subscribeEmbeddedPiSession } from "../../pi-embedded-subscribe.js";
import {
applyPiAutoCompactionGuard,
applyPiCompactionSettingsFromConfig,
} from "../../pi-settings.js";
import { createPreparedEmbeddedPiSettingsManager } from "../../pi-project-settings.js";
import { applyPiAutoCompactionGuard } from "../../pi-settings.js";
import {
createClientToolNameConflictError,
findClientToolNameConflicts,
@@ -1019,18 +1022,20 @@ export async function runEmbeddedAttempt(
modelId: params.modelId,
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();
}
const resourceLoader = new DefaultResourceLoader({
cwd: resolvedWorkspace,
agentDir,
settingsManager,
extensionFactories,
});
await resourceLoader.reload();
// DefaultResourceLoader.reload() rehydrates settings from disk and can drop OpenClaw
// compaction overrides applied in createPreparedEmbeddedPiSettingsManager.
applyPiCompactionSettingsFromConfig({
settingsManager,
cfg: params.config,
contextTokenBudget: params.contextTokenBudget,
});
// Get hook runner early so it's available when creating tools
const hookRunner = getGlobalHookRunner();

View File

@@ -23,6 +23,39 @@ describe("applyPiCompactionSettingsFromConfig", () => {
});
});
it("can restore reserveTokens after a simulated resource loader reload drops them below floor", () => {
const cfg = {
agents: { defaults: { compaction: { reserveTokensFloor: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR } } },
} as const;
let reserve = 16_384;
const keep = 20_000;
const settingsManager = {
getCompactionReserveTokens: () => reserve,
getCompactionKeepRecentTokens: () => keep,
applyOverrides: vi.fn((overrides: { compaction: { reserveTokens?: number } }) => {
if (overrides.compaction.reserveTokens !== undefined) {
reserve = overrides.compaction.reserveTokens;
}
}),
};
const first = applyPiCompactionSettingsFromConfig({
settingsManager,
cfg,
contextTokenBudget: 100_000,
});
expect(first.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR);
reserve = 16_384;
const second = applyPiCompactionSettingsFromConfig({
settingsManager,
cfg,
contextTokenBudget: 100_000,
});
expect(second.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR);
expect(reserve).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR);
});
it("does not override when already above floor and not in safeguard mode", () => {
const settingsManager = {
getCompactionReserveTokens: () => 32_000,