
















1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5+import { testing as cliBackendsTesting } from "../../agents/cli-backends.js";
6+import type { SessionEntry } from "../../config/sessions.js";
7+import {
8+clearMemoryPluginState,
9+registerMemoryCapability,
10+type MemoryFlushPlanResolver,
11+} from "../../plugins/memory-state.js";
12+import {
13+runPreflightCompactionIfNeeded,
14+setAgentRunnerMemoryTestDeps,
15+} from "./agent-runner-memory.js";
16+import { createTestFollowupRun, writeTestSessionStore } from "./agent-runner.test-fixtures.js";
17+import type { ReplyOperation } from "./reply-run-registry.js";
18+19+const compactEmbeddedAgentSessionMock = vi.fn();
20+21+function createReplyOperation(): ReplyOperation {
22+return {
23+key: "test",
24+sessionId: "session",
25+abortSignal: new AbortController().signal,
26+resetTriggered: false,
27+phase: "queued",
28+result: null,
29+setPhase: vi.fn(),
30+updateSessionId: vi.fn(),
31+attachBackend: vi.fn(),
32+detachBackend: vi.fn(),
33+retainFailureUntilComplete: vi.fn(),
34+complete: vi.fn(),
35+completeThen: vi.fn((afterClear: () => void) => {
36+afterClear();
37+}),
38+completeWithAfterClearBarrier: vi.fn(),
39+fail: vi.fn(),
40+abortByUser: vi.fn(),
41+abortForRestart: vi.fn(),
42+} as unknown as ReplyOperation;
43+}
44+45+function registerMemoryFlushPlanResolverForTest(resolver: MemoryFlushPlanResolver): void {
46+registerMemoryCapability("memory-core", { flushPlanResolver: resolver });
47+}
48+49+describe("runPreflightCompactionIfNeeded stale totalTokens gating", () => {
50+let rootDir = "";
51+52+beforeEach(async () => {
53+rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-preflight-stale-"));
54+registerMemoryFlushPlanResolverForTest(() => ({
55+softThresholdTokens: 4_000,
56+forceFlushTranscriptBytes: 1_000_000_000,
57+reserveTokensFloor: 20_000,
58+prompt: "Pre-compaction memory flush.\nNO_REPLY",
59+systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
60+relativePath: "memory/2023-11-14.md",
61+}));
62+compactEmbeddedAgentSessionMock.mockReset().mockResolvedValue({
63+ok: true,
64+compacted: true,
65+result: { tokensAfter: 42 },
66+});
67+setAgentRunnerMemoryTestDeps({
68+compactEmbeddedAgentSession: compactEmbeddedAgentSessionMock as never,
69+incrementCompactionCount: vi.fn() as never,
70+refreshQueuedFollowupSession: vi.fn() as never,
71+registerAgentRunContext: vi.fn() as never,
72+emitAgentEvent: vi.fn() as never,
73+});
74+});
75+76+afterEach(async () => {
77+setAgentRunnerMemoryTestDeps();
78+cliBackendsTesting.resetDepsForTest();
79+clearMemoryPluginState();
80+await fs.rm(rootDir, { recursive: true, force: true });
81+});
82+83+async function runWithEntry(sessionEntry: SessionEntry, sessionFile: string) {
84+return await runPreflightCompactionIfNeeded({
85+cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
86+followupRun: createTestFollowupRun({
87+sessionId: "session",
88+ sessionFile,
89+sessionKey: "agent:main:main",
90+}),
91+defaultModel: "anthropic/claude-opus-4-6",
92+agentCfgContextTokens: 100_000,
93+ sessionEntry,
94+sessionStore: { "agent:main:main": sessionEntry },
95+sessionKey: "agent:main:main",
96+storePath: path.join(rootDir, "sessions.json"),
97+isHeartbeat: false,
98+replyOperation: createReplyOperation(),
99+});
100+}
101+102+it("does not compact when totalTokens is large but stale and the real transcript is small", async () => {
103+const sessionFile = path.join(rootDir, "session.jsonl");
104+await fs.writeFile(
105+sessionFile,
106+`${JSON.stringify({ message: { role: "user", content: "x".repeat(2_000) } })}\n`,
107+"utf8",
108+);
109+const sessionEntry: SessionEntry = {
110+sessionId: "session",
111+ sessionFile,
112+updatedAt: Date.now(),
113+totalTokens: 200_000,
114+totalTokensFresh: false,
115+};
116+await writeTestSessionStore(path.join(rootDir, "sessions.json"), "agent:main:main", sessionEntry);
117+118+const entry = await runWithEntry(sessionEntry, sessionFile);
119+120+expect(entry).toBe(sessionEntry);
121+expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
122+});
123+124+it("compacts when totalTokens is large and fresh", async () => {
125+const sessionFile = path.join(rootDir, "session.jsonl");
126+await fs.writeFile(
127+sessionFile,
128+`${JSON.stringify({ message: { role: "user", content: "x".repeat(2_000) } })}\n`,
129+"utf8",
130+);
131+const sessionEntry: SessionEntry = {
132+sessionId: "session",
133+ sessionFile,
134+updatedAt: Date.now(),
135+totalTokens: 200_000,
136+totalTokensFresh: true,
137+};
138+await writeTestSessionStore(path.join(rootDir, "sessions.json"), "agent:main:main", sessionEntry);
139+140+await runWithEntry(sessionEntry, sessionFile);
141+142+expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
143+});
144+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。