mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 13:12:52 +00:00
* refactor: extract agent core package Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts. * refactor: extract shared llm runtime Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout. * refactor: remove pi runtime internals Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code. * refactor: tighten agent session runtime Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts. * refactor: remove static model and pi auth paths Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities. * refactor: remove legacy provider compat paths * docs: remove agent parity notes * fix: skip provider wildcard metadata parsing * refactor: share session extension sdk loading * refactor: inline acpx proxy error formatter * refactor: fold edit recovery into edit tool * fix: accept extension batch separator * test: align startup provider plugin expectations * fix: restore provider-scoped release discovery * test: align static asset packaging expectations * fix: run static provider catalogs during scoped discovery * fix: add provider entry catalogs for scoped live discovery * fix: load lightweight provider catalog entries * fix: refresh provider-scoped plugin metadata * fix: keep provider catalog entries on release live path * fix: keep static manifest models in release live checks * fix: harden release model discovery * fix: reduce OpenAI live cache probe reasoning * fix: disable OpenAI cache probe reasoning * ci: extend OpenAI gateway live timeout * fix: extend live gateway model budget * fix: stabilize release validation regressions * fix: honor provider aliases in model rows * fix: stabilize release validation lanes * fix: stabilize release memory qa * ci: stabilize release validation lanes * ci: prefer ipv4 for live docker node calls * fix: restore shared tool-call stream wrapper * ci: remove legacy pi test shard alias * fix: clean up embedded agent test drift * fix: stabilize runtime alias status * fix: clean up embedded agent ci drift * fix: restore release ci invariants * fix: clean up post-rebase runtime drift * fix: restore release ci checks * fix: restore release ci after rebase * fix: remove stale pi runtime path * test: align compaction runtime expectations * test: update plugin prerelease expectations * fix: handle claude live tool approvals * fix: stabilize release validation gates * fix: finish agent runtime import * test: finish post-rebase agent runtime mocks * fix: keep codex compaction native * fix: stabilize codex app-server hook tests * test: isolate codex diagnostic active run * test: remove codex diagnostic completion race # Conflicts: # extensions/codex/src/app-server/run-attempt.test.ts * ci: fix full release manifest performance run id * refactor: narrow llm plugin sdk boundary * chore: drop generated google boundary stamps * fix: repair rebase fallout * fix: clean up rebased runtime references * fix: decode codex jwt payloads as base64url * fix: preserve shipped pi runtime alias * fix: add scoped sdk virtual modules * fix: decode llm codex oauth jwt as base64url * fix: avoid stale vertex adc negative cache * fix: harden tool arg decoding and codeql path * fix: keep vertex adc negative checks live * refactor: consolidate codex jwt and edit helpers * fix: await codex oauth node runtime imports * fix: preserve sdk tool and notice contracts * fix: preserve shipped compat config boundaries * fix: align codex oauth callback host * fix: terminate agent-core loop streams on failure * fix: keep codex oauth callback alive during fallback * ci: include session tools in critical codeql scans * fix: keep Cloudflare Anthropic provider auth header * docs: redirect legacy pi runtime pages * fix: honor bundled web provider compat discovery * fix: protect session output spill files * fix: keep legacy agent dir env blocked * fix: contain auto-discovered skill symlinks * fix: harden agent core sdk proxy surfaces * fix: restore approval reaction sdk compat * fix: keep live docker runs bounded * fix: keep codex oauth redirect host aligned * fix: resolve post-rebase agent runtime drift * fix: redact anthropic oauth parse failures * fix: preserve responses strict tool shaping * fix: repair agent runtime rebase cleanup * docs: redirect retired parity pages * fix: bound auto-discovered resources to roots * fix: repair post-rebase agent test drift * fix: preserve bundled provider allowlist migration * fix: preserve manifest-owned provider aliases * fix: declare photon image dependency * fix: keep provider headers out of proxy body * fix: preserve shipped env aliases * fix: refresh control ui i18n generated state * fix: quote read fallback paths * fix: preview edits through configured backend * test: satisfy core test typecheck * fix: preserve ZAI usage auth fallback * test: repair codex diagnostic test * fix: repair agent runtime rebase drift * test: finish embedded runner import rename * fix: repair agent runtime rebase integrations * test: align compaction oauth fallback expectations * fix: allow sdk-auth session models * fix: update doctor tool schema import * fix: preserve bedrock plugin region * fix: stream harmony-like prose immediately * ci: include session runtime in codeql shards * fix: repair latest rebase integrations * fix: honor explicit codex websocket transport * fix: keep openai-compatible credentials provider-scoped * fix: refresh sdk api baseline after rebase * fix: route cli runtime aliases through openclaw harness * test: rename stale harness mock expectation * test: rename embedded agent overflow calls * test: clean embedded auth test wording * test: use openclaw stream types in deepinfra cache test * fix: refresh sdk api baseline on latest main * fix: honor bundled discovery compat allowlists * fix: refresh sdk api baseline after latest rebase * fix: remove stale rebase imports * test: rename stale model catalog mock * test: mock renamed doctor runtime modules * fix: map canonical kimi env auth * fix: use internal model registry in bench script * fix: migrate deepinfra provider catalog entry * fix: enforce builtin tool suppression * fix: route compaction auth and proxy payloads safely * refactor: prune unused llm registry leftovers * test: update codex hooks session import * test: fix model picker ci coverage * test: align model picker auth mock types
614 lines
22 KiB
TypeScript
614 lines
22 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./agent-compaction-constants.js";
|
|
import {
|
|
applyAgentAutoCompactionGuard,
|
|
applyAgentCompactionSettingsFromConfig,
|
|
DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR,
|
|
isSilentOverflowProneModel,
|
|
resolveEffectiveCompactionMode,
|
|
resolveCompactionReserveTokensFloor,
|
|
shouldDisableAgentAutoCompaction,
|
|
} from "./agent-settings.js";
|
|
|
|
describe("applyAgentCompactionSettingsFromConfig", () => {
|
|
it("bumps reserveTokens when below floor", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({ settingsManager });
|
|
|
|
expect(result.didOverride).toBe(true);
|
|
expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR },
|
|
});
|
|
});
|
|
|
|
it("can restore reserveTokens after a simulated resource loader reload drops them below floor", () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { reserveTokensFloor: DEFAULT_AGENT_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 = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg,
|
|
contextTokenBudget: 100_000,
|
|
});
|
|
expect(first.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
|
|
reserve = 16_384;
|
|
const second = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg,
|
|
contextTokenBudget: 100_000,
|
|
});
|
|
expect(second.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
expect(reserve).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
});
|
|
|
|
it("does not override when already above floor and not in safeguard mode", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 32_000,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: { agents: { defaults: { compaction: { mode: "default" } } } },
|
|
});
|
|
|
|
expect(result.didOverride).toBe(false);
|
|
expect(result.compaction.reserveTokens).toBe(32_000);
|
|
expect(settingsManager.applyOverrides).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("applies explicit reserveTokens but still enforces floor", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 10_000,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { reserveTokens: 12_000, reserveTokensFloor: 20_000 },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.compaction.reserveTokens).toBe(20_000);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: 20_000 },
|
|
});
|
|
});
|
|
|
|
it("applies keepRecentTokens when explicitly configured", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 20_000,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
keepRecentTokens: 15_000,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.compaction.keepRecentTokens).toBe(15_000);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { keepRecentTokens: 15_000 },
|
|
});
|
|
});
|
|
|
|
it("preserves current keepRecentTokens when safeguard mode leaves it unset", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 25_000,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: { agents: { defaults: { compaction: { mode: "safeguard" } } } },
|
|
});
|
|
|
|
expect(result.compaction.keepRecentTokens).toBe(20_000);
|
|
expect(settingsManager.applyOverrides).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("treats keepRecentTokens=0 as invalid and keeps the current setting", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 25_000,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: { agents: { defaults: { compaction: { mode: "safeguard", keepRecentTokens: 0 } } } },
|
|
});
|
|
|
|
expect(result.compaction.keepRecentTokens).toBe(20_000);
|
|
expect(settingsManager.applyOverrides).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("caps floor to context window ratio for small-context models", () => {
|
|
// Embedded runner default reserveTokens is 16 384. With a 16 384 context window
|
|
// the default floor (20 000) exceeds the window. The aligned cap
|
|
// computes: minPromptBudget = min(8_000, floor(16_384 * 0.5)) = 8_000,
|
|
// maxReserve = 16_384 - 8_000 = 8_384. Since current (16_384) > capped
|
|
// floor (8_384), no override is needed.
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
contextTokenBudget: 16_384,
|
|
});
|
|
|
|
// Without the cap, reserveTokens would be bumped to 20_000.
|
|
// With the cap, it stays at 16_384 (the current value).
|
|
expect(result.compaction.reserveTokens).toBe(16_384);
|
|
expect(result.compaction.reserveTokens).toBeLessThan(
|
|
DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR,
|
|
);
|
|
expect(result.didOverride).toBe(false);
|
|
expect(settingsManager.applyOverrides).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("applies capped floor over user-configured reserveTokens when default floor exceeds context window", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
// User sets reserveTokens=2048 but NOT reserveTokensFloor (default 20_000 applies).
|
|
// Pre-fix: target = max(2048, 20_000) = 20_000 → exceeds 16_384 context → infinite loop.
|
|
// Post-fix: floor capped to 8_384 → target = max(2048, 8_384) = 8_384 → works.
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { reserveTokens: 2_048 },
|
|
},
|
|
},
|
|
},
|
|
contextTokenBudget: 16_384,
|
|
});
|
|
|
|
expect(result.didOverride).toBe(true);
|
|
expect(result.compaction.reserveTokens).toBe(8_384); // capped floor wins over user's 2_048
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: 8_384 },
|
|
});
|
|
});
|
|
|
|
it("applies capped floor when current reserve is below it on small-context models", () => {
|
|
// Simulate an embedded runner default of 4 096 with a 16 384 context window.
|
|
// minPromptBudget = min(8_000, floor(16_384 * 0.5)) = 8_000.
|
|
// maxReserve = 16_384 - 8_000 = 8_384.
|
|
// Capped floor = min(20_000, 8_384) = 8_384.
|
|
// targetReserveTokens = max(4_096, 8_384) = 8_384 → override applied.
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 4_096,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
contextTokenBudget: 16_384,
|
|
});
|
|
|
|
const minPromptBudget = Math.min(
|
|
MIN_PROMPT_BUDGET_TOKENS,
|
|
Math.max(1, Math.floor(16_384 * MIN_PROMPT_BUDGET_RATIO)),
|
|
);
|
|
const expectedReserve = Math.max(0, 16_384 - minPromptBudget);
|
|
expect(result.didOverride).toBe(true);
|
|
expect(result.compaction.reserveTokens).toBe(expectedReserve);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: expectedReserve },
|
|
});
|
|
});
|
|
|
|
it("respects user-configured reserveTokens below capped floor for small models", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
// User explicitly sets reserveTokens=2048 and reserveTokensFloor=0.
|
|
// With contextTokenBudget=16384, the capped floor = min(0, 8192) = 0.
|
|
// targetReserveTokens = max(2048, 0) = 2048.
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { reserveTokens: 2_048, reserveTokensFloor: 0 },
|
|
},
|
|
},
|
|
},
|
|
contextTokenBudget: 16_384,
|
|
});
|
|
|
|
expect(result.compaction.reserveTokens).toBe(2_048);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: 2_048 },
|
|
});
|
|
});
|
|
|
|
it("does not cap floor for mid-size models when maxReserve exceeds default floor", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
// 32 768 context window → minPromptBudget = min(8_000, floor(32_768 * 0.5)) = 8_000.
|
|
// maxReserve = 32_768 - 8_000 = 24_768.
|
|
// Since 24_768 > 20_000 (DEFAULT_FLOOR), the floor is NOT capped and stays at 20_000.
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
contextTokenBudget: 32_768,
|
|
});
|
|
|
|
expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR },
|
|
});
|
|
});
|
|
|
|
it("does not cap floor when context window is large enough", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
// 200 000 context window → maxReserve = 200_000 - 8_000 = 192_000.
|
|
// floor (20 000) is well within that cap.
|
|
const result = applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
contextTokenBudget: 200_000,
|
|
});
|
|
|
|
expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
|
|
compaction: { reserveTokens: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR },
|
|
});
|
|
});
|
|
|
|
it("falls back to uncapped floor when contextTokenBudget is not provided", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 16_384,
|
|
getCompactionKeepRecentTokens: () => 20_000,
|
|
applyOverrides: vi.fn(),
|
|
};
|
|
|
|
// No contextTokenBudget → backward-compatible behavior, floor = 20 000.
|
|
const result = applyAgentCompactionSettingsFromConfig({ settingsManager });
|
|
|
|
expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR);
|
|
});
|
|
});
|
|
|
|
describe("resolveCompactionReserveTokensFloor", () => {
|
|
it("returns the default when config is missing", () => {
|
|
expect(resolveCompactionReserveTokensFloor()).toBe(
|
|
DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR,
|
|
);
|
|
});
|
|
|
|
it("accepts configured floors, including zero", () => {
|
|
expect(
|
|
resolveCompactionReserveTokensFloor({
|
|
agents: { defaults: { compaction: { reserveTokensFloor: 24_000 } } },
|
|
}),
|
|
).toBe(24_000);
|
|
expect(
|
|
resolveCompactionReserveTokensFloor({
|
|
agents: { defaults: { compaction: { reserveTokensFloor: 0 } } },
|
|
}),
|
|
).toBe(0);
|
|
});
|
|
});
|
|
describe("resolveEffectiveCompactionMode", () => {
|
|
it("defaults to default compaction mode", () => {
|
|
expect(resolveEffectiveCompactionMode()).toBe("default");
|
|
expect(resolveEffectiveCompactionMode({ agents: { defaults: { compaction: {} } } })).toBe(
|
|
"default",
|
|
);
|
|
expect(
|
|
resolveEffectiveCompactionMode({
|
|
agents: { defaults: { compaction: { mode: "default" } } },
|
|
}),
|
|
).toBe("default");
|
|
});
|
|
|
|
it("returns safeguard for explicit safeguard mode", () => {
|
|
expect(
|
|
resolveEffectiveCompactionMode({
|
|
agents: { defaults: { compaction: { mode: "safeguard" } } },
|
|
}),
|
|
).toBe("safeguard");
|
|
});
|
|
|
|
it("returns safeguard when a compaction provider is configured", () => {
|
|
expect(
|
|
resolveEffectiveCompactionMode({
|
|
agents: { defaults: { compaction: { provider: "deepseek" } } },
|
|
}),
|
|
).toBe("safeguard");
|
|
expect(
|
|
resolveEffectiveCompactionMode({
|
|
agents: { defaults: { compaction: { mode: "default", provider: "deepseek" } } },
|
|
}),
|
|
).toBe("safeguard");
|
|
});
|
|
});
|
|
|
|
describe("isSilentOverflowProneModel", () => {
|
|
// Reporter's repro shape: openrouter routing to z-ai/glm. Both the bare
|
|
// `z-ai/...` form and the `openrouter/z-ai/...` qualified form must hit.
|
|
it("flags z-ai-prefixed model ids regardless of qualifier", () => {
|
|
expect(isSilentOverflowProneModel({ provider: "openrouter", modelId: "z-ai/glm-5.1" })).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
isSilentOverflowProneModel({ provider: "openrouter", modelId: "openrouter/z-ai/glm-5" }),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("flags a config-set z.ai provider regardless of model id", () => {
|
|
expect(isSilentOverflowProneModel({ provider: "z.ai", modelId: "glm-5.1" })).toBe(true);
|
|
expect(isSilentOverflowProneModel({ provider: "z-ai", modelId: "glm-5.1" })).toBe(true);
|
|
});
|
|
|
|
it("flags a direct api.z.ai baseUrl via endpointClass", () => {
|
|
expect(
|
|
isSilentOverflowProneModel({
|
|
provider: "openai",
|
|
modelId: "glm-5.1",
|
|
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
// openclaw#75799 reporter's setup: an OpenAI-compatible in-house gateway
|
|
// exposing Zhipu's GLM family directly (model id `glm-5.1`, no `z-ai/`
|
|
// qualifier, custom baseUrl that is not api.z.ai). Catch the bare GLM
|
|
// family name so direct gateway deployments hit the guard regardless of
|
|
// what `provider` field the user picked — gateways relabel the upstream
|
|
// identity, so `provider` here can be anything from `openai` to a custom
|
|
// string. False positives only disable OpenClaw runtime's secondary compaction path;
|
|
// OpenClaw's preemptive compaction continues to handle real overflow.
|
|
it("flags bare glm- model ids without a namespace prefix, regardless of provider", () => {
|
|
expect(isSilentOverflowProneModel({ provider: "custom", modelId: "glm-5.1" })).toBe(true);
|
|
expect(isSilentOverflowProneModel({ provider: "custom", modelId: "glm-4.7" })).toBe(true);
|
|
expect(isSilentOverflowProneModel({ provider: "openai", modelId: "glm-5.1" })).toBe(true);
|
|
expect(isSilentOverflowProneModel({ provider: "openrouter", modelId: "glm-5.1" })).toBe(true);
|
|
});
|
|
|
|
// Detection is intentionally narrow to z.ai-style accounting. Namespaced GLM
|
|
// ids that route through providers with their own overflow accounting must
|
|
// NOT be flagged — those hosts may not exhibit the z.ai silent-overflow
|
|
// shape, and disabling embedded auto-compaction for them would over-broaden the
|
|
// kill surface beyond the reproducible repro.
|
|
it("does not flag namespaced GLM ids routed through non-z.ai hosts", () => {
|
|
expect(
|
|
isSilentOverflowProneModel({ provider: "ollama", modelId: "ollama/glm-5.1:cloud" }),
|
|
).toBe(false);
|
|
expect(
|
|
isSilentOverflowProneModel({ provider: "opencode-go", modelId: "opencode-go/glm-5.1" }),
|
|
).toBe(false);
|
|
});
|
|
|
|
// shared model runtime's overflow.ts only documents z.ai as the silent-overflow style. We
|
|
// intentionally do NOT extend the guard to anthropic/openai/google/openrouter-
|
|
// anthropic routes — adding them without a reproducible repro would broaden
|
|
// the kill surface and regress baseline behavior for those providers.
|
|
it("does not flag anthropic, openai, google or other routes", () => {
|
|
expect(
|
|
isSilentOverflowProneModel({ provider: "anthropic", modelId: "claude-sonnet-4.6" }),
|
|
).toBe(false);
|
|
expect(isSilentOverflowProneModel({ provider: "openai", modelId: "gpt-5.5" })).toBe(false);
|
|
expect(
|
|
isSilentOverflowProneModel({
|
|
provider: "openrouter",
|
|
modelId: "anthropic/claude-sonnet-4.6",
|
|
}),
|
|
).toBe(false);
|
|
expect(isSilentOverflowProneModel({ provider: "google", modelId: "gemini-2.5-pro" })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("treats missing fields as not silent-overflow-prone", () => {
|
|
expect(isSilentOverflowProneModel({})).toBe(false);
|
|
expect(
|
|
isSilentOverflowProneModel({ provider: undefined, modelId: undefined, baseUrl: null }),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("shouldDisableAgentAutoCompaction", () => {
|
|
it("returns false with no owner, default mode, and ordinary provider behavior", () => {
|
|
expect(shouldDisableAgentAutoCompaction({})).toBe(false);
|
|
expect(shouldDisableAgentAutoCompaction({ compactionMode: "default" })).toBe(false);
|
|
expect(
|
|
shouldDisableAgentAutoCompaction({
|
|
contextEngineInfo: { id: "legacy", name: "Legacy", ownsCompaction: false },
|
|
compactionMode: "default",
|
|
silentOverflowProneProvider: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("returns true when a context engine owns compaction", () => {
|
|
expect(
|
|
shouldDisableAgentAutoCompaction({
|
|
contextEngineInfo: { id: "third-party", name: "Third-party", ownsCompaction: true },
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns true when effective compaction mode is safeguard", () => {
|
|
expect(shouldDisableAgentAutoCompaction({ compactionMode: "safeguard" })).toBe(true);
|
|
});
|
|
|
|
it("returns true for silent-overflow-prone providers", () => {
|
|
expect(shouldDisableAgentAutoCompaction({ silentOverflowProneProvider: true })).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("applyAgentAutoCompactionGuard", () => {
|
|
// Direct repro of openclaw#75799: shared model runtime's silent-overflow detection misfires
|
|
// on a successful turn against z.ai-style providers, triggering OpenClaw runtime's
|
|
// _runAutoCompaction from inside Session.prompt() and reassigning
|
|
// agent.state.messages between the runner's prompt.submitted trajectory
|
|
// event and the provider request. Disabling embedded auto-compaction here keeps
|
|
// state.messages intact; OpenClaw's preemptive compaction continues to
|
|
// handle real overflow on its own path.
|
|
it("disables embedded auto-compaction for silent-overflow-prone providers", () => {
|
|
const setCompactionEnabled = vi.fn();
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 20_000,
|
|
getCompactionKeepRecentTokens: () => 4_000,
|
|
applyOverrides: () => {},
|
|
setCompactionEnabled,
|
|
};
|
|
|
|
const result = applyAgentAutoCompactionGuard({
|
|
settingsManager,
|
|
silentOverflowProneProvider: true,
|
|
});
|
|
|
|
expect(result).toEqual({ supported: true, disabled: true });
|
|
expect(setCompactionEnabled).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it("disables embedded auto-compaction when a context engine plugin owns compaction", () => {
|
|
const setCompactionEnabled = vi.fn();
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 20_000,
|
|
getCompactionKeepRecentTokens: () => 4_000,
|
|
applyOverrides: () => {},
|
|
setCompactionEnabled,
|
|
};
|
|
|
|
const result = applyAgentAutoCompactionGuard({
|
|
settingsManager,
|
|
contextEngineInfo: {
|
|
id: "third-party",
|
|
name: "Third-party Context Engine",
|
|
version: "0.1.0",
|
|
ownsCompaction: true,
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual({ supported: true, disabled: true });
|
|
expect(setCompactionEnabled).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it("disables embedded auto-compaction when provider config forces safeguard mode", () => {
|
|
const setCompactionEnabled = vi.fn();
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 20_000,
|
|
getCompactionKeepRecentTokens: () => 4_000,
|
|
applyOverrides: () => {},
|
|
setCompactionEnabled,
|
|
};
|
|
|
|
const result = applyAgentAutoCompactionGuard({
|
|
settingsManager,
|
|
compactionMode: resolveEffectiveCompactionMode({
|
|
agents: { defaults: { compaction: { provider: "deepseek" } } },
|
|
}),
|
|
});
|
|
|
|
expect(result).toEqual({ supported: true, disabled: true });
|
|
expect(setCompactionEnabled).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
// Default-mode runs against ordinary providers must keep OpenClaw runtime's auto-compaction
|
|
// enabled. Disabling it across the board would silently remove OpenClaw runtime's
|
|
// overflow-recovery path inside Session.prompt() for users who are not
|
|
// affected by z.ai's silent-overflow accounting.
|
|
it("leaves embedded auto-compaction alone for non-z.ai providers without engine ownership", () => {
|
|
const setCompactionEnabled = vi.fn();
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 20_000,
|
|
getCompactionKeepRecentTokens: () => 4_000,
|
|
applyOverrides: () => {},
|
|
setCompactionEnabled,
|
|
};
|
|
|
|
const result = applyAgentAutoCompactionGuard({
|
|
settingsManager,
|
|
contextEngineInfo: {
|
|
id: "legacy",
|
|
name: "Legacy Context Engine",
|
|
version: "1.0.0",
|
|
},
|
|
silentOverflowProneProvider: false,
|
|
});
|
|
|
|
expect(result).toEqual({ supported: true, disabled: false });
|
|
expect(setCompactionEnabled).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports unsupported when the settings manager has no setCompactionEnabled hook", () => {
|
|
const settingsManager = {
|
|
getCompactionReserveTokens: () => 20_000,
|
|
getCompactionKeepRecentTokens: () => 4_000,
|
|
applyOverrides: () => {},
|
|
};
|
|
|
|
const result = applyAgentAutoCompactionGuard({
|
|
settingsManager,
|
|
silentOverflowProneProvider: true,
|
|
});
|
|
|
|
expect(result).toEqual({ supported: false, disabled: false });
|
|
});
|
|
});
|