




















@@ -1,8 +1,10 @@
11import { describe, expect, it, vi } from "vitest";
22import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./pi-compaction-constants.js";
33import {
4+applyPiAutoCompactionGuard,
45applyPiCompactionSettingsFromConfig,
56DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR,
7+isSilentOverflowProneModel,
68resolveCompactionReserveTokensFloor,
79} from "./pi-settings.js";
810@@ -345,3 +347,179 @@ describe("resolveCompactionReserveTokensFloor", () => {
345347).toBe(0);
346348});
347349});
350+351+describe("isSilentOverflowProneModel", () => {
352+// Reporter's repro shape: openrouter routing to z-ai/glm. Both the bare
353+// `z-ai/...` form and the `openrouter/z-ai/...` qualified form must hit.
354+it("flags z-ai-prefixed model ids regardless of qualifier", () => {
355+expect(isSilentOverflowProneModel({ provider: "openrouter", modelId: "z-ai/glm-5.1" })).toBe(
356+true,
357+);
358+expect(
359+isSilentOverflowProneModel({ provider: "openrouter", modelId: "openrouter/z-ai/glm-5" }),
360+).toBe(true);
361+});
362+363+it("flags a config-set z.ai provider regardless of model id", () => {
364+expect(isSilentOverflowProneModel({ provider: "z.ai", modelId: "glm-5.1" })).toBe(true);
365+expect(isSilentOverflowProneModel({ provider: "z-ai", modelId: "glm-5.1" })).toBe(true);
366+});
367+368+it("flags a direct api.z.ai baseUrl via endpointClass", () => {
369+expect(
370+isSilentOverflowProneModel({
371+provider: "openai",
372+modelId: "glm-5.1",
373+baseUrl: "https://api.z.ai/api/coding/paas/v4",
374+}),
375+).toBe(true);
376+});
377+378+// openclaw#75799 reporter's setup: an OpenAI-compatible in-house gateway
379+// exposing Zhipu's GLM family directly (model id `glm-5.1`, no `z-ai/`
380+// qualifier, custom baseUrl that is not api.z.ai). Catch the bare GLM
381+// family name so direct gateway deployments hit the guard regardless of
382+// what `provider` field the user picked — gateways relabel the upstream
383+// identity, so `provider` here can be anything from `openai` to a custom
384+// string. False positives only disable Pi's secondary compaction path;
385+// OpenClaw's preemptive compaction continues to handle real overflow.
386+it("flags bare glm- model ids without a namespace prefix, regardless of provider", () => {
387+expect(isSilentOverflowProneModel({ provider: "custom", modelId: "glm-5.1" })).toBe(true);
388+expect(isSilentOverflowProneModel({ provider: "custom", modelId: "glm-4.7" })).toBe(true);
389+expect(isSilentOverflowProneModel({ provider: "openai", modelId: "glm-5.1" })).toBe(true);
390+expect(isSilentOverflowProneModel({ provider: "openrouter", modelId: "glm-5.1" })).toBe(true);
391+});
392+393+// Detection is intentionally narrow to z.ai-style accounting. Namespaced GLM
394+// ids that route through providers with their own overflow accounting must
395+// NOT be flagged — those hosts may not exhibit the z.ai silent-overflow
396+// shape, and disabling Pi auto-compaction for them would over-broaden the
397+// kill surface beyond the reproducible repro.
398+it("does not flag namespaced GLM ids routed through non-z.ai hosts", () => {
399+expect(
400+isSilentOverflowProneModel({ provider: "ollama", modelId: "ollama/glm-5.1:cloud" }),
401+).toBe(false);
402+expect(
403+isSilentOverflowProneModel({ provider: "opencode-go", modelId: "opencode-go/glm-5.1" }),
404+).toBe(false);
405+});
406+407+// pi-ai's overflow.ts only documents z.ai as the silent-overflow style. We
408+// intentionally do NOT extend the guard to anthropic/openai/google/openrouter-
409+// anthropic routes — adding them without a reproducible repro would broaden
410+// the kill surface and regress baseline behavior for those providers.
411+it("does not flag anthropic, openai, google or other routes", () => {
412+expect(
413+isSilentOverflowProneModel({ provider: "anthropic", modelId: "claude-sonnet-4.6" }),
414+).toBe(false);
415+expect(isSilentOverflowProneModel({ provider: "openai", modelId: "gpt-5.5" })).toBe(false);
416+expect(
417+isSilentOverflowProneModel({
418+provider: "openrouter",
419+modelId: "anthropic/claude-sonnet-4.6",
420+}),
421+).toBe(false);
422+expect(isSilentOverflowProneModel({ provider: "google", modelId: "gemini-2.5-pro" })).toBe(
423+false,
424+);
425+});
426+427+it("treats missing fields as not silent-overflow-prone", () => {
428+expect(isSilentOverflowProneModel({})).toBe(false);
429+expect(
430+isSilentOverflowProneModel({ provider: undefined, modelId: undefined, baseUrl: null }),
431+).toBe(false);
432+});
433+});
434+435+describe("applyPiAutoCompactionGuard", () => {
436+// Direct repro of openclaw#75799: pi-ai's silent-overflow detection misfires
437+// on a successful turn against z.ai-style providers, triggering Pi's
438+// _runAutoCompaction from inside Session.prompt() and reassigning
439+// agent.state.messages between the runner's prompt.submitted trajectory
440+// event and the provider request. Disabling Pi auto-compaction here keeps
441+// state.messages intact; OpenClaw's preemptive compaction continues to
442+// handle real overflow on its own path.
443+it("disables Pi auto-compaction for silent-overflow-prone providers", () => {
444+const setCompactionEnabled = vi.fn();
445+const settingsManager = {
446+getCompactionReserveTokens: () => 20_000,
447+getCompactionKeepRecentTokens: () => 4_000,
448+applyOverrides: () => {},
449+ setCompactionEnabled,
450+};
451+452+const result = applyPiAutoCompactionGuard({
453+ settingsManager,
454+silentOverflowProneProvider: true,
455+});
456+457+expect(result).toEqual({ supported: true, disabled: true });
458+expect(setCompactionEnabled).toHaveBeenCalledWith(false);
459+});
460+461+it("disables Pi auto-compaction when a context engine plugin owns compaction", () => {
462+const setCompactionEnabled = vi.fn();
463+const settingsManager = {
464+getCompactionReserveTokens: () => 20_000,
465+getCompactionKeepRecentTokens: () => 4_000,
466+applyOverrides: () => {},
467+ setCompactionEnabled,
468+};
469+470+const result = applyPiAutoCompactionGuard({
471+ settingsManager,
472+contextEngineInfo: {
473+id: "third-party",
474+name: "Third-party Context Engine",
475+version: "0.1.0",
476+ownsCompaction: true,
477+},
478+});
479+480+expect(result).toEqual({ supported: true, disabled: true });
481+expect(setCompactionEnabled).toHaveBeenCalledWith(false);
482+});
483+484+// Default-mode runs against ordinary providers must keep Pi's auto-compaction
485+// enabled. Disabling it across the board would silently remove Pi's
486+// overflow-recovery path inside Session.prompt() for users who are not
487+// affected by z.ai's silent-overflow accounting.
488+it("leaves Pi auto-compaction alone for non-z.ai providers without engine ownership", () => {
489+const setCompactionEnabled = vi.fn();
490+const settingsManager = {
491+getCompactionReserveTokens: () => 20_000,
492+getCompactionKeepRecentTokens: () => 4_000,
493+applyOverrides: () => {},
494+ setCompactionEnabled,
495+};
496+497+const result = applyPiAutoCompactionGuard({
498+ settingsManager,
499+contextEngineInfo: {
500+id: "legacy",
501+name: "Legacy Context Engine",
502+version: "1.0.0",
503+},
504+silentOverflowProneProvider: false,
505+});
506+507+expect(result).toEqual({ supported: true, disabled: false });
508+expect(setCompactionEnabled).not.toHaveBeenCalled();
509+});
510+511+it("reports unsupported when the settings manager has no setCompactionEnabled hook", () => {
512+const settingsManager = {
513+getCompactionReserveTokens: () => 20_000,
514+getCompactionKeepRecentTokens: () => 4_000,
515+applyOverrides: () => {},
516+};
517+518+const result = applyPiAutoCompactionGuard({
519+ settingsManager,
520+silentOverflowProneProvider: true,
521+});
522+523+expect(result).toEqual({ supported: false, disabled: false });
524+});
525+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。