diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1d2d6bcb5..d9bfbb23ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index a7b10aced41..39948d67111 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -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, diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.resource-loader.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.resource-loader.test.ts new file mode 100644 index 00000000000..cda4d3d7d80 --- /dev/null +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.resource-loader.test.ts @@ -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), + }), + }), + ); + }); +}); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts index 9aa30e1db83..b760e74ebd0 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts @@ -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", () => ({ diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index ef5f969fa9a..20685b2d1f3 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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(); diff --git a/src/agents/pi-settings.test.ts b/src/agents/pi-settings.test.ts index 879dafe7399..4214549e3b3 100644 --- a/src/agents/pi-settings.test.ts +++ b/src/agents/pi-settings.test.ts @@ -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,