mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
3689 lines
125 KiB
TypeScript
3689 lines
125 KiB
TypeScript
// Tests agent runner memory flush and persisted memory context handling.
|
|
import fsCore from "node:fs";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { testing as cliBackendsTesting } from "../../agents/cli-backends.test-support.js";
|
|
import type { runEmbeddedAgentEntry } from "../../agents/embedded-agent-runner/run-entry.js";
|
|
import type { EmbeddedAgentRunResult } from "../../agents/embedded-agent-runner/types.js";
|
|
import type { SessionEntry } from "../../config/sessions.js";
|
|
import {
|
|
loadSessionEntry,
|
|
readTranscriptStatsSync,
|
|
upsertSessionEntry,
|
|
} from "../../config/sessions/session-accessor.js";
|
|
import { replaceSqliteTranscriptEvents } from "../../config/sessions/session-accessor.sqlite.js";
|
|
import { resolveSessionStorePathForScope } from "../../config/sessions/session-store-path.js";
|
|
import {
|
|
clearMemoryPluginState,
|
|
registerMemoryCapability,
|
|
type MemoryFlushPlanResolver,
|
|
} from "../../plugins/memory-state.test-fixtures.js";
|
|
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
|
|
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
|
import type { TemplateContext } from "../templating.js";
|
|
import type { ReplyPayload } from "../types.js";
|
|
import { runMemoryFlushIfNeeded, runPreflightCompactionIfNeeded } from "./agent-runner-memory.js";
|
|
import { setAgentRunnerMemoryTestDeps } from "./agent-runner-memory.test-support.js";
|
|
import { createTestFollowupRun, writeTestSessionStore } from "./agent-runner.test-fixtures.js";
|
|
import type { ReplyOperation } from "./reply-run-registry.js";
|
|
|
|
const compactEmbeddedAgentSessionMock = vi.fn();
|
|
const runWithModelFallbackMock = vi.fn();
|
|
const runEmbeddedAgentEntryMock = vi.fn();
|
|
const runEmbeddedAgentMock = vi.fn();
|
|
const refreshQueuedFollowupSessionMock = vi.fn();
|
|
const incrementCompactionCountMock = vi.fn();
|
|
const ensureSelectedAgentHarnessPluginMock = vi.fn();
|
|
const ensureMemoryFlushTargetFileMock = vi.fn();
|
|
const emitAgentEventMock = vi.fn();
|
|
const registerAgentRunContextMock = vi.fn();
|
|
const TEST_MAX_FLUSH_FAILURES = 3;
|
|
|
|
function registerMemoryFlushPlanResolverForTest(resolver: MemoryFlushPlanResolver): void {
|
|
registerMemoryCapability("memory-core", { flushPlanResolver: resolver });
|
|
}
|
|
|
|
function registerClaudeCliBackend(ownsNativeCompaction = false): void {
|
|
cliBackendsTesting.setDepsForTest({
|
|
resolveRuntimeCliBackends: () => [
|
|
{
|
|
id: "claude-cli",
|
|
modelProvider: "anthropic",
|
|
pluginId: "anthropic",
|
|
config: { command: "claude" },
|
|
ownsNativeCompaction,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
type TestReplyOperation = ReplyOperation & {
|
|
setPhase: ReturnType<typeof vi.fn<ReplyOperation["setPhase"]>>;
|
|
updateSessionId: ReturnType<typeof vi.fn<ReplyOperation["updateSessionId"]>>;
|
|
};
|
|
|
|
function createReplyOperation(): TestReplyOperation {
|
|
const now = Date.now();
|
|
return {
|
|
key: "test",
|
|
sessionId: "session",
|
|
abortSignal: new AbortController().signal,
|
|
staleExpiryReason: undefined,
|
|
resetTriggered: false,
|
|
terminalRecovery: false,
|
|
acceptedSteeredInboundAudio: false,
|
|
startedAtMs: now,
|
|
lastActivityAtMs: now,
|
|
phase: "queued",
|
|
result: null,
|
|
recordActivity: vi.fn(),
|
|
hasOwnedSessionId: vi.fn((sessionId: string) => sessionId === "session"),
|
|
setPhase: vi.fn<ReplyOperation["setPhase"]>(),
|
|
updateSessionId: vi.fn<ReplyOperation["updateSessionId"]>(),
|
|
updateSessionKey: vi.fn<ReplyOperation["updateSessionKey"]>(),
|
|
attachBackend: vi.fn(),
|
|
detachBackend: vi.fn(),
|
|
freezeAbort: vi.fn(),
|
|
retainFailureUntilComplete: vi.fn(),
|
|
complete: vi.fn(),
|
|
completeThen: vi.fn((afterClear: () => void) => {
|
|
afterClear();
|
|
}),
|
|
completeWithAfterClearBarrier: vi.fn(),
|
|
fail: vi.fn(),
|
|
abortByUser: vi.fn(() => true),
|
|
abortForRestart: vi.fn(() => true),
|
|
markTerminalRecovery: vi.fn(),
|
|
markAcceptedSteeredInboundAudio: vi.fn(),
|
|
markWaitingForDeferredMaintenance: vi.fn(),
|
|
markDeferredMaintenanceWaitEnded: vi.fn(),
|
|
markWaitingForGlobalLane: vi.fn(),
|
|
markGlobalLaneWaitEnded: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function loadMainSessionEntry(storePath: string): SessionEntry {
|
|
const entry = loadSessionEntry({ storePath, sessionKey: "main" });
|
|
if (!entry) {
|
|
throw new Error("expected persisted main session entry");
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
async function writeTestSessionTranscript(params: {
|
|
rootDir: string;
|
|
events: Parameters<typeof replaceSqliteTranscriptEvents>[1];
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
}): Promise<void> {
|
|
const sessionId = params.sessionId ?? "session";
|
|
const sessionKey = params.sessionKey ?? "main";
|
|
const scope = {
|
|
agentId: "main",
|
|
sessionId,
|
|
sessionKey,
|
|
storePath: path.join(params.rootDir, "sessions.json"),
|
|
};
|
|
await upsertSessionEntry(scope, { sessionId, updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, params.events);
|
|
}
|
|
|
|
type RefreshQueuedFollowupSessionParams = {
|
|
key?: string;
|
|
previousSessionId?: string;
|
|
nextSessionId?: string;
|
|
nextSessionFile?: string;
|
|
};
|
|
|
|
type ModelFallbackParams = {
|
|
provider?: string;
|
|
model?: string;
|
|
abortSignal?: AbortSignal;
|
|
agentId?: string;
|
|
sessionId?: string;
|
|
sessionKey?: string;
|
|
fallbacksOverride?: unknown[];
|
|
requestedRouteResolution?: "raw" | "resolved";
|
|
userLockedAuthProfileId?: string;
|
|
resolveAgentHarnessRuntimeOverride?: (provider: string, model: string) => string | undefined;
|
|
prepareAgentHarnessRuntime?: (params: {
|
|
provider: string;
|
|
model: string;
|
|
agentHarnessRuntimeOverride?: string;
|
|
}) => Promise<void> | void;
|
|
run: (
|
|
provider: string,
|
|
model: string,
|
|
options?: {
|
|
allowTransientCooldownProbe?: boolean;
|
|
isFinalFallbackAttempt?: boolean;
|
|
},
|
|
) => Promise<EmbeddedAgentRunResult>;
|
|
};
|
|
|
|
type EmbeddedAgentParams = {
|
|
provider?: string;
|
|
model?: string;
|
|
thinkLevel?: string;
|
|
agentHarnessId?: string;
|
|
agentHarnessRuntimeOverride?: string;
|
|
authProfileId?: unknown;
|
|
authProfileIdSource?: unknown;
|
|
prompt?: string;
|
|
transcriptPrompt?: string;
|
|
memoryFlushWritePath?: string;
|
|
silentExpected?: boolean;
|
|
extraSystemPrompt?: string;
|
|
bootstrapPromptWarningSignaturesSeen?: string[];
|
|
bootstrapPromptWarningSignature?: string;
|
|
abortSignal?: AbortSignal;
|
|
isFinalFallbackAttempt?: boolean;
|
|
onAgentEvent?: (evt: {
|
|
stream: string;
|
|
data: { isError?: boolean; name?: string; phase?: string };
|
|
}) => void;
|
|
};
|
|
|
|
type CompactEmbeddedAgentSessionParams = {
|
|
agentId?: string;
|
|
agentHarnessId?: string;
|
|
authProfileId?: string;
|
|
authProfileIdSource?: "auto" | "user";
|
|
contextTokenBudget?: number;
|
|
sessionKey?: string;
|
|
sandboxSessionKey?: string;
|
|
currentTokenCount?: number;
|
|
cwd?: string;
|
|
force?: boolean;
|
|
forcePreflight?: boolean;
|
|
modelSelectionLocked?: boolean;
|
|
preflightRequired?: boolean;
|
|
preflightCompactionTrigger?: string;
|
|
sessionFile?: string;
|
|
sessionId?: string;
|
|
trigger?: string;
|
|
};
|
|
|
|
function requireRefreshQueuedFollowupSessionCall(index = 0) {
|
|
const call = refreshQueuedFollowupSessionMock.mock.calls[index]?.[0] as
|
|
| RefreshQueuedFollowupSessionParams
|
|
| undefined;
|
|
if (!call) {
|
|
throw new Error(`refreshQueuedFollowupSession call ${index} missing`);
|
|
}
|
|
return call;
|
|
}
|
|
|
|
function requireModelFallbackCall(index = 0) {
|
|
const call = runWithModelFallbackMock.mock.calls[index]?.[0] as ModelFallbackParams | undefined;
|
|
if (!call) {
|
|
throw new Error(`runWithModelFallback call ${index} missing`);
|
|
}
|
|
return call;
|
|
}
|
|
|
|
function requireEmbeddedAgentCall(index = 0) {
|
|
const call = runEmbeddedAgentMock.mock.calls[index]?.[0] as EmbeddedAgentParams | undefined;
|
|
if (!call) {
|
|
throw new Error(`runEmbeddedAgent call ${index} missing`);
|
|
}
|
|
return call;
|
|
}
|
|
|
|
function requireCompactEmbeddedAgentSessionCall(index = 0) {
|
|
const call = compactEmbeddedAgentSessionMock.mock.calls[index]?.[0] as
|
|
| CompactEmbeddedAgentSessionParams
|
|
| undefined;
|
|
if (!call) {
|
|
throw new Error(`compactEmbeddedAgentSession call ${index} missing`);
|
|
}
|
|
return call;
|
|
}
|
|
|
|
describe("runMemoryFlushIfNeeded", () => {
|
|
let rootDir = "";
|
|
|
|
beforeEach(async () => {
|
|
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-unit-"));
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 20_000,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
runWithModelFallbackMock.mockReset().mockImplementation(async ({ provider, model, run }) => ({
|
|
result: await run(provider, model),
|
|
provider,
|
|
model,
|
|
attempts: [],
|
|
}));
|
|
runEmbeddedAgentEntryMock
|
|
.mockReset()
|
|
.mockImplementation(
|
|
async (params: Parameters<typeof runEmbeddedAgentEntry<EmbeddedAgentRunResult>>[0]) => {
|
|
const fallbackResult = (await runWithModelFallbackMock({
|
|
...params.selection,
|
|
...params.identity,
|
|
abortSignal: params.abortSignal,
|
|
resolveAgentHarnessRuntimeOverride: params.harness.resolveRuntimeOverride,
|
|
prepareAgentHarnessRuntime: async ({
|
|
provider,
|
|
model,
|
|
agentHarnessRuntimeOverride,
|
|
}: {
|
|
provider: string;
|
|
model: string;
|
|
agentHarnessRuntimeOverride?: string;
|
|
}) => {
|
|
await ensureSelectedAgentHarnessPluginMock({
|
|
config: params.selection.cfg,
|
|
provider,
|
|
modelId: model,
|
|
agentId: params.identity.agentId,
|
|
sessionKey: params.harness.sessionKey,
|
|
agentHarnessId: agentHarnessRuntimeOverride,
|
|
agentHarnessRuntimeOverride,
|
|
workspaceDir: params.harness.workspaceDir,
|
|
});
|
|
},
|
|
run: (
|
|
provider: string,
|
|
model: string,
|
|
options?: ModelFallbackParams["run"] extends (
|
|
provider: string,
|
|
model: string,
|
|
options?: infer TOptions,
|
|
) => Promise<EmbeddedAgentRunResult>
|
|
? TOptions
|
|
: never,
|
|
) =>
|
|
params.runCandidate(provider, model, {
|
|
allowTransientCooldownProbe: options?.allowTransientCooldownProbe,
|
|
isFinalFallbackAttempt: options?.isFinalFallbackAttempt,
|
|
isFallbackRetry: false,
|
|
contextEngineLogicalTurnLease: {} as never,
|
|
onContextEngineTurnCandidate: () => {},
|
|
}),
|
|
})) as {
|
|
outcome?: "completed" | "exhausted";
|
|
result: EmbeddedAgentRunResult;
|
|
provider: string;
|
|
model: string;
|
|
attempts: [];
|
|
};
|
|
return {
|
|
...fallbackResult,
|
|
outcome: fallbackResult.outcome ?? ("completed" as const),
|
|
terminal: {
|
|
outcome: { reason: "completed" as const, status: "ok" as const },
|
|
metadata: {},
|
|
},
|
|
settleSessionOverride: async () => undefined,
|
|
};
|
|
},
|
|
);
|
|
compactEmbeddedAgentSessionMock.mockReset().mockResolvedValue({
|
|
ok: true,
|
|
compacted: true,
|
|
result: { tokensAfter: 42 },
|
|
});
|
|
runEmbeddedAgentMock.mockReset().mockResolvedValue({ payloads: [], meta: {} });
|
|
refreshQueuedFollowupSessionMock.mockReset();
|
|
ensureMemoryFlushTargetFileMock.mockReset().mockResolvedValue(undefined);
|
|
ensureSelectedAgentHarnessPluginMock.mockReset().mockResolvedValue(undefined);
|
|
emitAgentEventMock.mockReset();
|
|
registerAgentRunContextMock.mockReset();
|
|
incrementCompactionCountMock.mockReset().mockImplementation(async (params) => {
|
|
const sessionKey = String(params.sessionKey ?? "");
|
|
if (!sessionKey || !params.sessionStore?.[sessionKey]) {
|
|
return undefined;
|
|
}
|
|
const previous = params.sessionStore[sessionKey] as SessionEntry;
|
|
const nextEntry: SessionEntry = {
|
|
...previous,
|
|
compactionCount: (previous.compactionCount ?? 0) + 1,
|
|
};
|
|
if (typeof params.newSessionId === "string" && params.newSessionId) {
|
|
nextEntry.sessionId = params.newSessionId;
|
|
}
|
|
params.sessionStore[sessionKey] = nextEntry;
|
|
if (typeof params.storePath === "string") {
|
|
await writeTestSessionStore(params.storePath, sessionKey, nextEntry);
|
|
}
|
|
return nextEntry.compactionCount;
|
|
});
|
|
setAgentRunnerMemoryTestDeps({
|
|
compactEmbeddedAgentSession: compactEmbeddedAgentSessionMock as never,
|
|
runEmbeddedAgentEntry: runEmbeddedAgentEntryMock as never,
|
|
runEmbeddedAgent: runEmbeddedAgentMock as never,
|
|
ensureMemoryFlushTargetFile: ensureMemoryFlushTargetFileMock as never,
|
|
refreshQueuedFollowupSession: refreshQueuedFollowupSessionMock as never,
|
|
incrementCompactionCount: incrementCompactionCountMock as never,
|
|
registerAgentRunContext: registerAgentRunContextMock as never,
|
|
emitAgentEvent: emitAgentEventMock as never,
|
|
randomUUID: () => "00000000-0000-0000-0000-000000000001",
|
|
now: () => 1_700_000_000_000,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
setAgentRunnerMemoryTestDeps();
|
|
cliBackendsTesting.resetDepsForTest();
|
|
setActivePluginRegistry(createEmptyPluginRegistry());
|
|
clearMemoryPluginState();
|
|
await fs.rm(rootDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("runs a memory flush turn, rotates after compaction, and persists metadata", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionKey = "main";
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
const sessionStore = { [sessionKey]: sessionEntry };
|
|
await writeTestSessionStore(storePath, sessionKey, sessionEntry);
|
|
|
|
runEmbeddedAgentMock.mockImplementationOnce(
|
|
async (params: {
|
|
onAgentEvent?: (evt: { stream: string; data: { phase: string } }) => void;
|
|
}) => {
|
|
params.onAgentEvent?.({ stream: "compaction", data: { phase: "end" } });
|
|
return {
|
|
payloads: [],
|
|
meta: { agentMeta: { sessionId: "session-rotated" } },
|
|
};
|
|
},
|
|
);
|
|
|
|
const followupRun = createTestFollowupRun({
|
|
authProfileId: "anthropic:work",
|
|
authProfileIdSource: "user",
|
|
});
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
memoryFlush: {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun,
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result.outcome).toBe("completed");
|
|
expect(result.sessionEntry?.sessionId).toBe("session-rotated");
|
|
expect(followupRun.run.sessionId).toBe("session-rotated");
|
|
expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1);
|
|
expect(requireModelFallbackCall().userLockedAuthProfileId).toBe("anthropic:work");
|
|
const flushCall = requireEmbeddedAgentCall();
|
|
expect(flushCall.prompt).toContain("Pre-compaction memory flush.");
|
|
expect(flushCall.transcriptPrompt).toBe("");
|
|
expect(flushCall.prompt).not.toBe(flushCall.transcriptPrompt);
|
|
expect(flushCall.memoryFlushWritePath).toMatch(/^memory\/\d{4}-\d{2}-\d{2}\.md$/);
|
|
expect(flushCall.silentExpected).toBe(true);
|
|
expect(registerAgentRunContextMock).toHaveBeenCalledWith(
|
|
"00000000-0000-0000-0000-000000000001",
|
|
expect.objectContaining({
|
|
isControlUiVisible: false,
|
|
projectSessionActive: false,
|
|
projectSessionLifecycle: false,
|
|
sessionId: "session",
|
|
sessionKey,
|
|
}),
|
|
);
|
|
expect(ensureMemoryFlushTargetFileMock).toHaveBeenCalledWith({
|
|
workspaceDir: followupRun.run.workspaceDir,
|
|
relativePath: flushCall.memoryFlushWritePath,
|
|
});
|
|
expect(ensureMemoryFlushTargetFileMock.mock.invocationCallOrder[0]).toBeLessThan(
|
|
runEmbeddedAgentMock.mock.invocationCallOrder[0] ?? 0,
|
|
);
|
|
expect(refreshQueuedFollowupSessionMock).toHaveBeenCalledTimes(1);
|
|
const refreshCall = requireRefreshQueuedFollowupSessionCall();
|
|
expect(refreshCall.key).toBe(sessionKey);
|
|
expect(refreshCall.previousSessionId).toBe("session");
|
|
expect(refreshCall.nextSessionId).toBe("session-rotated");
|
|
expect(refreshCall.nextSessionFile).toBe(sessionKey);
|
|
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(persisted.sessionId).toBe("session-rotated");
|
|
expect(persisted.compactionCount).toBe(2);
|
|
expect(persisted.memoryFlush).toEqual({ kind: "succeeded", compactionCount: 1 });
|
|
});
|
|
|
|
it("records the least-trusted provenance across a multi-write flush", async () => {
|
|
const recordWriteProvenance = vi.fn(async () => {});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 20_000,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
recordWriteProvenance,
|
|
}));
|
|
const targetPath = path.join(rootDir, "memory", "2023-11-14.md");
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
await fs.writeFile(targetPath, "trusted existing line\n", "utf8");
|
|
runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
|
|
await fs.appendFile(targetPath, "first untrusted line\n", "utf8");
|
|
params.onAgentEvent?.({
|
|
stream: "tool",
|
|
data: { name: "write", phase: "result", isError: false },
|
|
});
|
|
await fs.appendFile(targetPath, "second untrusted line\n", "utf8");
|
|
params.onAgentEvent?.({
|
|
stream: "tool",
|
|
data: { name: "write", phase: "result", isError: false },
|
|
});
|
|
return { payloads: [], meta: {} };
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({ workspaceDir: rootDir, senderIsOwner: false }),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(recordWriteProvenance).toHaveBeenCalledOnce();
|
|
expect(recordWriteProvenance).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
contentBefore: "trusted existing line\n",
|
|
contentAfter: "trusted existing line\nfirst untrusted line\nsecond untrusted line\n",
|
|
originClass: "untrusted",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("downgrades an owner-directed flush after a network-tainted embedded turn", async () => {
|
|
const recordWriteProvenance = vi.fn(async () => {});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 20_000,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
recordWriteProvenance,
|
|
}));
|
|
const storePath = path.join(rootDir, "tainted-owner-session.json");
|
|
const sessionKey = "agent:main:main";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
{
|
|
type: "message",
|
|
message: { role: "user", content: "Research this", __openclaw: { senderIsOwner: true } },
|
|
},
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "toolResult",
|
|
content: "untrusted page",
|
|
__openclaw: { resultContentSource: "network" },
|
|
},
|
|
},
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "network-derived answer",
|
|
__openclaw: { turnTainted: true },
|
|
},
|
|
},
|
|
// Force the bounded SQLite tail to lose the turn boundary and taint marker.
|
|
// A truncated active turn must remain conservatively tainted.
|
|
...Array.from({ length: 512 }, (_, index) => ({
|
|
type: "custom",
|
|
data: { index },
|
|
})),
|
|
]);
|
|
const targetPath = path.join(rootDir, "memory", "2023-11-14.md");
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
|
|
await fs.writeFile(targetPath, "network-derived memory\n", "utf8");
|
|
params.onAgentEvent?.({
|
|
stream: "tool",
|
|
data: { name: "write", phase: "result", isError: false },
|
|
});
|
|
return { payloads: [], meta: {} };
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
workspaceDir: rootDir,
|
|
sessionId: "session",
|
|
sessionKey,
|
|
senderIsOwner: true,
|
|
}),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(recordWriteProvenance).toHaveBeenCalledWith(
|
|
expect.objectContaining({ originClass: "untrusted" }),
|
|
);
|
|
});
|
|
|
|
it("revalidates immutable Ultra for each memory-flush fallback candidate", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionKey = "main";
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
thinkingLevel: "ultra",
|
|
};
|
|
const sessionStore = { [sessionKey]: sessionEntry };
|
|
await writeTestSessionStore(storePath, sessionKey, sessionEntry);
|
|
runWithModelFallbackMock.mockImplementationOnce(
|
|
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => {
|
|
await params.run("openai", "gpt-5.6-sol");
|
|
return {
|
|
result: await params.run("demo", "basic"),
|
|
provider: "demo",
|
|
model: "basic",
|
|
attempts: [],
|
|
};
|
|
},
|
|
);
|
|
const followupRun = createTestFollowupRun();
|
|
followupRun.run.provider = "openai";
|
|
followupRun.run.model = "gpt-5.6-sol";
|
|
followupRun.run.thinkLevel = "ultra";
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { memoryFlush: {} },
|
|
models: {
|
|
"openai/gpt-5.6-sol": { agentRuntime: { id: "openclaw" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun,
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "openai/gpt-5.6-sol",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(runEmbeddedAgentMock.mock.calls.map((call) => call[0]?.thinkLevel)).toEqual([
|
|
"ultra",
|
|
"high",
|
|
]);
|
|
expect(followupRun.run.thinkLevel).toBe("ultra");
|
|
});
|
|
|
|
it("preserves thinking for runtime-discovered Ollama memory-flush models", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionKey = "main";
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
thinkingLevel: "high",
|
|
};
|
|
const sessionStore = { [sessionKey]: sessionEntry };
|
|
await writeTestSessionStore(storePath, sessionKey, sessionEntry);
|
|
const followupRun = createTestFollowupRun({
|
|
provider: "ollama",
|
|
model: "qwen3.5:4b",
|
|
});
|
|
followupRun.run.thinkLevel = "high";
|
|
followupRun.run.thinkingCatalog = [{ provider: "ollama", id: "qwen3.5:4b", reasoning: true }];
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun,
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "ollama/qwen3.5:4b",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(requireEmbeddedAgentCall().thinkLevel).toBe("high");
|
|
});
|
|
|
|
it("keeps catalog-adopted sessions on Codex for memory flush turns", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "catalog-adopted-session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
agentHarnessId: "codex",
|
|
agentRuntimeOverride: "claude-cli",
|
|
modelSelectionLocked: true,
|
|
pluginExtensions: {
|
|
codex: {
|
|
supervision: {
|
|
sourceThreadId: "019f-codex-thread",
|
|
modelLocked: true,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { memoryFlush: {} },
|
|
models: {
|
|
"anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-6",
|
|
sessionId: sessionEntry.sessionId,
|
|
sessionKey: "main",
|
|
}),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result.outcome).toBe("completed");
|
|
expect(requireEmbeddedAgentCall()).toMatchObject({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-6",
|
|
agentHarnessId: "codex",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
});
|
|
});
|
|
|
|
it("counts resolved error payloads as failed memory flushes", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
const sessionStore = { main: sessionEntry };
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
const visibleErrorPayloads: Array<{ text?: string; isError?: boolean }> = [];
|
|
runEmbeddedAgentMock.mockImplementationOnce(
|
|
async (params: {
|
|
onAgentEvent?: (event: { stream: string; data: { phase: string } }) => void;
|
|
}) => {
|
|
params.onAgentEvent?.({ stream: "compaction", data: { phase: "end" } });
|
|
return {
|
|
payloads: [
|
|
{ text: "normal silent maintenance reply" },
|
|
{
|
|
text: "⚠️ write failed: Memory flush writes are restricted to memory/2023-11-14.md; use that path only.",
|
|
isError: true,
|
|
},
|
|
],
|
|
meta: { agentMeta: { sessionId: "session-rotated" } },
|
|
};
|
|
},
|
|
);
|
|
const followupRun = createTestFollowupRun();
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun,
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onVisibleErrorPayloads: (payloads) => {
|
|
visibleErrorPayloads.push(...payloads);
|
|
},
|
|
});
|
|
|
|
expect(visibleErrorPayloads).toEqual([
|
|
{
|
|
text: "⚠️ write failed: Memory flush writes are restricted to memory/2023-11-14.md; use that path only.",
|
|
isError: true,
|
|
},
|
|
]);
|
|
expect(requireModelFallbackCall().userLockedAuthProfileId).toBeUndefined();
|
|
expect(result.outcome).toBe("failed");
|
|
expect(result.sessionEntry?.sessionId).toBe("session-rotated");
|
|
expect(followupRun.run.sessionId).toBe("session-rotated");
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(persisted.sessionId).toBe("session-rotated");
|
|
expect(persisted.compactionCount).toBe(2);
|
|
expect(persisted.memoryFlush).toEqual({ kind: "failed", failureCount: 1 });
|
|
});
|
|
|
|
it("reports restricted memory-flush write failures for visible delivery", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
const visibleErrorPayloads: Array<{ text?: string; isError?: boolean }> = [];
|
|
runWithModelFallbackMock.mockRejectedValueOnce(
|
|
new Error(
|
|
"write failed: Memory flush writes are restricted to memory/2023-11-14.md; use that path only.",
|
|
),
|
|
);
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
authProfileId: "anthropic:auto",
|
|
authProfileIdSource: "auto",
|
|
}),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onVisibleErrorPayloads: (payloads) => {
|
|
visibleErrorPayloads.push(...payloads);
|
|
},
|
|
});
|
|
|
|
expect(visibleErrorPayloads).toEqual([
|
|
{
|
|
text: "⚠️ write failed: Memory flush writes are restricted to memory/2023-11-14.md; use that path only.",
|
|
isError: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("surfaces generic non-abort memory-flush failures so cron meta.error is populated (regression: #80755)", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
const visibleErrorPayloads: Array<{ text?: string; isError?: boolean }> = [];
|
|
runWithModelFallbackMock.mockRejectedValueOnce(
|
|
new Error("provider timed out after 60s while flushing memory"),
|
|
);
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onVisibleErrorPayloads: (payloads) => {
|
|
visibleErrorPayloads.push(...payloads);
|
|
},
|
|
});
|
|
|
|
expect(visibleErrorPayloads).toEqual([
|
|
{
|
|
text: "⚠️ provider timed out after 60s while flushing memory",
|
|
isError: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("redacts and caps generic visible memory-flush failures before delivery", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
const visibleErrorPayloads: Array<{ text?: string; isError?: boolean }> = [];
|
|
const token = "sk-abcdefghijklmnopqrstuv";
|
|
runWithModelFallbackMock.mockRejectedValueOnce(
|
|
new Error(`provider failed with Authorization: Bearer ${token} ${"🚀".repeat(400)}`),
|
|
);
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onVisibleErrorPayloads: (payloads) => {
|
|
visibleErrorPayloads.push(...payloads);
|
|
},
|
|
});
|
|
|
|
const [payload] = visibleErrorPayloads;
|
|
expect(payload?.isError).toBe(true);
|
|
expect(payload?.text).toMatch(/^⚠️ provider failed with Authorization: Bearer /);
|
|
expect(payload?.text).not.toContain(token);
|
|
expect(payload?.text?.length).toBeLessThanOrEqual(600);
|
|
expect(payload?.text?.endsWith("🚀…")).toBe(true);
|
|
});
|
|
|
|
it("does not surface user-abort errors as visible payloads (regression: #80755)", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
const visibleErrorPayloads: Array<{ text?: string; isError?: boolean }> = [];
|
|
const abortErr = new Error("operation aborted by user");
|
|
abortErr.name = "AbortError";
|
|
runWithModelFallbackMock.mockRejectedValueOnce(abortErr);
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onVisibleErrorPayloads: (payloads) => {
|
|
visibleErrorPayloads.push(...payloads);
|
|
},
|
|
});
|
|
|
|
expect(visibleErrorPayloads).toEqual([]);
|
|
});
|
|
|
|
it("increments and UTF-16-safely persists a capped non-abort flush failure", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
const failureMessage = `${"a".repeat(198)}🚀tail`;
|
|
runWithModelFallbackMock.mockRejectedValueOnce(new Error(failureMessage));
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(result.outcome).toBe("failed");
|
|
expect(persisted.memoryFlush).toEqual({ kind: "failed", failureCount: 1 });
|
|
expect(emitAgentEventMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
stream: "lifecycle",
|
|
data: expect.objectContaining({
|
|
phase: "memory_flush_failed",
|
|
attempt: 1,
|
|
maxAttempts: TEST_MAX_FLUSH_FAILURES,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not track failure on abort error", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
const abortErr = new Error("operation aborted by user");
|
|
abortErr.name = "AbortError";
|
|
runWithModelFallbackMock.mockRejectedValueOnce(abortErr);
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(result.outcome).toBe("failed");
|
|
expect(persisted.memoryFlush).toBeUndefined();
|
|
});
|
|
|
|
it("clears failure counters on successful flush", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
memoryFlush: { kind: "failed", failureCount: 2 },
|
|
};
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(result.outcome).toBe("completed");
|
|
expect(persisted.memoryFlush).toEqual({ kind: "succeeded", compactionCount: 1 });
|
|
});
|
|
|
|
it("marks flush as completed after MAX_FLUSH_FAILURES to break retry loop", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
memoryFlush: { kind: "failed", failureCount: TEST_MAX_FLUSH_FAILURES - 1 },
|
|
};
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
runWithModelFallbackMock.mockRejectedValueOnce(new Error("provider crashed during flush"));
|
|
|
|
const visibleErrorPayloads: ReplyPayload[] = [];
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onVisibleErrorPayloads: (payloads) => {
|
|
visibleErrorPayloads.push(...payloads);
|
|
},
|
|
});
|
|
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(result.outcome).toBe("exhausted");
|
|
expect(persisted.memoryFlush).toEqual({ kind: "succeeded", compactionCount: 1 });
|
|
expect(emitAgentEventMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
stream: "lifecycle",
|
|
data: expect.objectContaining({
|
|
phase: "memory_flush_exhausted",
|
|
attempt: TEST_MAX_FLUSH_FAILURES,
|
|
maxAttempts: TEST_MAX_FLUSH_FAILURES,
|
|
}),
|
|
}),
|
|
);
|
|
expect(visibleErrorPayloads[0]).toEqual(
|
|
expect.objectContaining({
|
|
text: expect.stringContaining("skipping for this cycle"),
|
|
isError: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("retries flush on subsequent messages until MAX_FLUSH_FAILURES", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
runWithModelFallbackMock.mockRejectedValue(new Error("provider crashed during flush"));
|
|
|
|
const params = {
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off" as const,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
};
|
|
|
|
await runMemoryFlushIfNeeded(params);
|
|
await runMemoryFlushIfNeeded({ ...params, replyOperation: createReplyOperation() });
|
|
|
|
expect(runWithModelFallbackMock).toHaveBeenCalledTimes(2);
|
|
|
|
const persisted = loadMainSessionEntry(storePath);
|
|
expect(persisted.memoryFlush).toEqual({ kind: "failed", failureCount: 2 });
|
|
});
|
|
|
|
it("next message retries flush after failure", async () => {
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
await writeTestSessionStore(storePath, "main", sessionEntry);
|
|
runWithModelFallbackMock.mockRejectedValueOnce(new Error("provider crashed during flush"));
|
|
|
|
const params = {
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-7",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off" as const,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
};
|
|
|
|
await runMemoryFlushIfNeeded(params);
|
|
await runMemoryFlushIfNeeded({ ...params, replyOperation: createReplyOperation() });
|
|
|
|
expect(runWithModelFallbackMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("runs memory flush on the configured maintenance model without active fallbacks", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 20_000,
|
|
model: "ollama/qwen3:8b",
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
const replyOperation = createReplyOperation();
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
model: {
|
|
primary: "anthropic/claude",
|
|
fallbacks: ["openai/gpt-5.4"],
|
|
},
|
|
models: {
|
|
"ollama/qwen3:8b": { alias: "memory-flush" },
|
|
"openrouter/qwen3:8b": { alias: "qwen3:8b" },
|
|
},
|
|
compaction: {
|
|
memoryFlush: {
|
|
model: "ollama/qwen3:8b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({ provider: "anthropic", model: "claude" }),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(runWithModelFallbackMock).toHaveBeenCalledTimes(1);
|
|
const fallbackCall = requireModelFallbackCall();
|
|
expect(fallbackCall.provider).toBe("ollama");
|
|
expect(fallbackCall.model).toBe("qwen3:8b");
|
|
expect(fallbackCall.requestedRouteResolution).toBe("raw");
|
|
expect(fallbackCall.abortSignal).toBe(replyOperation.abortSignal);
|
|
expect(fallbackCall.sessionId).toBe("session");
|
|
expect(fallbackCall.fallbacksOverride).toEqual([]);
|
|
expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1);
|
|
const agentCall = requireEmbeddedAgentCall();
|
|
expect(agentCall.provider).toBe("ollama");
|
|
expect(agentCall.model).toBe("qwen3:8b");
|
|
expect(agentCall.abortSignal).toBe(replyOperation.abortSignal);
|
|
expect(agentCall.authProfileId).toBeUndefined();
|
|
expect(agentCall.authProfileIdSource).toBeUndefined();
|
|
});
|
|
|
|
it("loads the selected harness before memory-flush fallback preflight", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
memoryFlush: {},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
agentRuntimeOverride: "codex",
|
|
};
|
|
const runtimePolicySessionKey = "agent:main:telegram:default:direct:12345";
|
|
runWithModelFallbackMock.mockImplementationOnce(
|
|
async (params: {
|
|
provider: string;
|
|
model: string;
|
|
run: (
|
|
provider: string,
|
|
model: string,
|
|
options?: { isFinalFallbackAttempt?: boolean },
|
|
) => Promise<unknown>;
|
|
}) => ({
|
|
result: await params.run(params.provider, params.model, {
|
|
isFinalFallbackAttempt: false,
|
|
}),
|
|
provider: params.provider,
|
|
model: params.model,
|
|
attempts: [],
|
|
}),
|
|
);
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg,
|
|
followupRun: createTestFollowupRun({
|
|
agentId: "main",
|
|
sessionKey: "main",
|
|
runtimePolicySessionKey,
|
|
workspaceDir: "/workspace",
|
|
provider: "openai",
|
|
model: "gpt-5.4",
|
|
}),
|
|
sessionCtx: { Provider: "telegram" } as unknown as TemplateContext,
|
|
defaultModel: "openai/gpt-5.4",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
runtimePolicySessionKey,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const fallbackCall = requireModelFallbackCall();
|
|
expect(fallbackCall.agentId).toBe("main");
|
|
expect(fallbackCall.sessionKey).toBe(runtimePolicySessionKey);
|
|
expect(fallbackCall.resolveAgentHarnessRuntimeOverride?.("openai", "gpt-5.4")).toBe("codex");
|
|
expect(requireEmbeddedAgentCall().isFinalFallbackAttempt).toBe(false);
|
|
|
|
await fallbackCall.prepareAgentHarnessRuntime?.({
|
|
provider: "openai",
|
|
model: "gpt-5.4",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
});
|
|
|
|
expect(ensureSelectedAgentHarnessPluginMock).toHaveBeenCalledWith({
|
|
config: cfg,
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
agentId: "main",
|
|
sessionKey: runtimePolicySessionKey,
|
|
agentHarnessId: "codex",
|
|
agentHarnessRuntimeOverride: "codex",
|
|
workspaceDir: "/workspace",
|
|
});
|
|
});
|
|
|
|
it("ignores stale runtime pins before memory-flush fallback preflight", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
agentRuntimeOverride: "unsupported-runtime",
|
|
};
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
provider: "openai",
|
|
model: "gpt-5.4",
|
|
}),
|
|
sessionCtx: { Provider: "telegram" } as unknown as TemplateContext,
|
|
defaultModel: "openai/gpt-5.4",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(
|
|
requireModelFallbackCall().resolveAgentHarnessRuntimeOverride?.("openai", "gpt-5.4"),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("skips memory flush for CLI providers", async () => {
|
|
const registry = createEmptyPluginRegistry();
|
|
registry.cliBackends.push({
|
|
pluginId: "test-codex-cli",
|
|
source: "test",
|
|
backend: { id: "codex-cli", config: { command: "codex" } },
|
|
});
|
|
setActivePluginRegistry(registry);
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: {},
|
|
followupRun: createTestFollowupRun({ provider: "codex-cli" }),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "codex-cli/gpt-5.5",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result).toEqual({ sessionEntry, outcome: "skipped" });
|
|
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips memory flush for incognito sessions", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
incognito: true,
|
|
sessionId: "incognito-session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "webchat" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result).toEqual({ sessionEntry, outcome: "skipped" });
|
|
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
|
expect(ensureMemoryFlushTargetFileMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips memory flush for an incognito key after process-local state is gone", async () => {
|
|
const sessionKey = "agent:main:dashboard:incognito-deleted-memory";
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "rematerialized-session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun(),
|
|
sessionCtx: { Provider: "webchat" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result).toEqual({ sessionEntry, outcome: "skipped" });
|
|
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips memory flush for compatible CLI session runtime pins", async () => {
|
|
registerClaudeCliBackend();
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
agentRuntimeOverride: "claude-cli",
|
|
};
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-6",
|
|
}),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result).toEqual({ sessionEntry, outcome: "skipped" });
|
|
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses runtime policy session key when checking memory-flush sandbox writability", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
};
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
sandbox: {
|
|
mode: "non-main",
|
|
scope: "agent",
|
|
workspaceAccess: "ro",
|
|
},
|
|
compaction: {
|
|
memoryFlush: {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
sessionKey: "agent:main:main",
|
|
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
|
}),
|
|
sessionCtx: { Provider: "telegram" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(result).toEqual({ sessionEntry, outcome: "skipped" });
|
|
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues when preflight compaction reports the session is already under target", async () => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
compacted: false,
|
|
reason: "already under target",
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
agentHarnessId: "openclaw",
|
|
modelSelectionLocked: true,
|
|
};
|
|
const onCompactionNotice = vi.fn();
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onCompactionNotice,
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
expect(requireCompactEmbeddedAgentSessionCall()).toMatchObject({
|
|
trigger: "budget",
|
|
force: true,
|
|
forcePreflight: true,
|
|
preflightRequired: true,
|
|
preflightCompactionTrigger: "tokens",
|
|
deferOwningContextEngineCompaction: false,
|
|
contextTokenBudget: 100,
|
|
agentHarnessId: "openclaw",
|
|
modelSelectionLocked: true,
|
|
});
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(1, "start");
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(2, "skipped");
|
|
|
|
onCompactionNotice.mockClear();
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: false,
|
|
compacted: false,
|
|
reason: "no real conversation messages",
|
|
});
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onCompactionNotice,
|
|
}),
|
|
).rejects.toThrow("Preflight compaction required but failed: no real conversation messages");
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(1, "start");
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(2, "incomplete");
|
|
});
|
|
|
|
it("fails when required preflight context-engine compaction is deferred to background maintenance", async () => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
compacted: false,
|
|
reason: "deferred to background context-engine maintenance",
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
};
|
|
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
}),
|
|
).rejects.toThrow(
|
|
"Preflight compaction required but failed: deferred to background context-engine maintenance",
|
|
);
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("passes runtime policy session key to preflight compaction sandbox resolution", async () => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:main",
|
|
cwd: "/tmp/task-repo",
|
|
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.sessionKey).toBe("agent:main:main");
|
|
expect(compactCall.cwd).toBe("/tmp/task-repo");
|
|
expect(compactCall.sandboxSessionKey).toBe("agent:main:telegram:default:direct:12345");
|
|
});
|
|
|
|
it.each([
|
|
["stale_thread_binding", "thread not found: <codex-thread-id>"],
|
|
["missing_thread_binding", "no thread binding for session"],
|
|
])(
|
|
"fails required preflight compaction after native harness %s failure",
|
|
async (failureReason, reason) => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: false,
|
|
compacted: false,
|
|
reason,
|
|
failure: { reason: failureReason },
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
};
|
|
const sessionStore = { "agent:main:telegram:group:redacted": sessionEntry };
|
|
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:telegram:group:redacted",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "agent:main:telegram:group:redacted",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
}),
|
|
).rejects.toThrow(`Preflight compaction required but failed: ${reason}`);
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("fails required preflight compaction after an unstructured thread-not-found failure", async () => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: false,
|
|
compacted: false,
|
|
reason: "thread not found: <codex-thread-id>",
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
};
|
|
const sessionStore = { "agent:main:telegram:group:redacted": sessionEntry };
|
|
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:telegram:group:redacted",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "agent:main:telegram:group:redacted",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
}),
|
|
).rejects.toThrow(
|
|
"Preflight compaction required but failed: thread not found: <codex-thread-id>",
|
|
);
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still fails preflight compaction for non-binding native harness failures", async () => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: false,
|
|
compacted: false,
|
|
reason: "auth profile mismatch",
|
|
failure: { reason: "auth_profile_mismatch" },
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
};
|
|
const sessionStore = { "agent:main:telegram:group:redacted": sessionEntry };
|
|
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:telegram:group:redacted",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "agent:main:telegram:group:redacted",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
}),
|
|
).rejects.toThrow("Preflight compaction required but failed: auth profile mismatch");
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(["user", "auto"] as const)(
|
|
"passes resolved context budget and $authProfileIdSource auth profile to preflight compaction",
|
|
async (authProfileIdSource) => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 245_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
authProfileId: "anthropic:claude@martian.engineering",
|
|
authProfileIdSource,
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-6",
|
|
sessionKey: "agent:main:main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 258_000,
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.authProfileId).toBe("anthropic:claude@martian.engineering");
|
|
expect(compactCall.authProfileIdSource).toBe(authProfileIdSource);
|
|
expect(compactCall.contextTokenBudget).toBe(258_000);
|
|
},
|
|
);
|
|
it("preflight compacts a fresh session when the current prompt estimate pushes the next request over budget", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 0,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 10,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const storePath = path.join(rootDir, "preflight-fresh-sessions.json");
|
|
const sessionKey = "agent:main:main";
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 985,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
await upsertSessionEntry({ agentId: "main", sessionKey, storePath }, sessionEntry);
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude",
|
|
sessionKey,
|
|
}),
|
|
promptForEstimate: "Please summarize the entire design discussion above. ".repeat(8),
|
|
defaultModel: "anthropic/claude",
|
|
agentCfgContextTokens: 1000,
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
it("does not preflight compact a fresh session when only accumulated output tokens are large and the latest output keeps the request under budget", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 0,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 10,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 985,
|
|
outputTokens: 50_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude",
|
|
sessionKey: "agent:main:main",
|
|
}),
|
|
promptForEstimate: "",
|
|
defaultModel: "anthropic/claude",
|
|
agentCfgContextTokens: 1000,
|
|
sessionEntry,
|
|
sessionStore: { "agent:main:main": sessionEntry },
|
|
sessionKey: "agent:main:main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
it("stops at unavailable context and accepts only a later valid transcript snapshot", async () => {
|
|
const sessionKey = "agent:main:main";
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const oldCumulative = {
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "old cumulative turn",
|
|
usage: { input: 128_814, output: 3_000, cacheRead: 992_953, totalTokens: 1_124_767 },
|
|
},
|
|
};
|
|
const unavailable = {
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "usage unavailable",
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
contextUsage: { state: "unavailable" },
|
|
},
|
|
},
|
|
};
|
|
await writeTestSessionTranscript({ rootDir, sessionKey, events: [oldCumulative, unavailable] });
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
compactionCount: 0,
|
|
};
|
|
const sessionStore = { [sessionKey]: sessionEntry };
|
|
const run = () =>
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude",
|
|
sessionId: "session",
|
|
sessionKey,
|
|
}),
|
|
promptForEstimate: "",
|
|
defaultModel: "anthropic/claude",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
await run();
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
sessionKey,
|
|
events: [
|
|
oldCumulative,
|
|
unavailable,
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "valid later turn",
|
|
usage: { input: 67_932, output: 2_000, cacheRead: 18_944, totalTokens: 88_876 },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await run();
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
expect(requireCompactEmbeddedAgentSessionCall().currentTokenCount).toBe(88_876);
|
|
});
|
|
it("ignores unversioned fresh state and legacy CLI usage on the first upgraded turn", async () => {
|
|
const sessionKey = "agent:main:main";
|
|
const storePath = path.join(rootDir, "sessions.json");
|
|
const legacyCli = {
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
api: "cli",
|
|
content: "legacy cumulative turn",
|
|
usage: { input: 128_814, output: 3_000, cacheRead: 992_953, totalTokens: 1_124_767 },
|
|
},
|
|
};
|
|
await writeTestSessionTranscript({ rootDir, sessionKey, events: [legacyCli] });
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 1_124_767,
|
|
totalTokensFresh: true,
|
|
compactionCount: 0,
|
|
};
|
|
const sessionStore = { [sessionKey]: sessionEntry };
|
|
const run = () =>
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude",
|
|
sessionId: "session",
|
|
sessionKey,
|
|
}),
|
|
promptForEstimate: "",
|
|
defaultModel: "anthropic/claude",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
await run();
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
sessionKey,
|
|
events: [
|
|
legacyCli,
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
api: "cli",
|
|
content: "repaired exact turn",
|
|
usage: {
|
|
input: 67_932,
|
|
output: 2_000,
|
|
cacheRead: 18_944,
|
|
totalTokens: 88_876,
|
|
contextUsage: {
|
|
state: "available",
|
|
promptTokens: 86_876,
|
|
totalTokens: 88_876,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await run();
|
|
|
|
expect(requireCompactEmbeddedAgentSessionCall().currentTokenCount).toBe(88_876);
|
|
});
|
|
it("updates the active preflight run after transcript rotation", async () => {
|
|
const sessionFile = path.join(rootDir, "session.jsonl");
|
|
const successorFile = path.join(rootDir, "session-rotated.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
sessionKey: "agent:main:main",
|
|
events: [{ type: "message", message: { role: "user", content: "x".repeat(5_000) } }],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 1,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
compacted: true,
|
|
result: {
|
|
tokensAfter: 42,
|
|
sessionId: "session-rotated",
|
|
sessionFile: successorFile,
|
|
},
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
const sessionStore = { "agent:main:main": sessionEntry };
|
|
const followupRun = createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "agent:main:main",
|
|
});
|
|
const replyOperation = createReplyOperation();
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "agent:main:main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(entry?.sessionId).toBe("session-rotated");
|
|
expect(entry?.sessionFile).toBeUndefined();
|
|
expect(followupRun.run.sessionId).toBe("session-rotated");
|
|
expect(followupRun.run.sessionFile).toBe("agent:main:main");
|
|
expect(replyOperation.updateSessionId).toHaveBeenCalledWith("session-rotated");
|
|
expect(refreshQueuedFollowupSessionMock).toHaveBeenCalledWith({
|
|
key: "agent:main:main",
|
|
previousSessionId: "session",
|
|
nextSessionId: "session-rotated",
|
|
nextSessionFile: "agent:main:main",
|
|
});
|
|
});
|
|
|
|
it("includes recent output tokens when deciding preflight compaction", async () => {
|
|
const sessionFile = path.join(rootDir, "session-usage.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "large answer",
|
|
usage: { input: 90_000, output: 10_000 },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.currentTokenCount).toBeGreaterThanOrEqual(100_000);
|
|
});
|
|
|
|
it("keeps nonzero unavailable output as growth after the previous exact snapshot", async () => {
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "large answer",
|
|
usage: {
|
|
input: 128_814,
|
|
output: 10_000,
|
|
cacheRead: 992_953,
|
|
totalTokens: 1_131_767,
|
|
contextUsage: { state: "unavailable" },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 70_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({ sessionId: "session", sessionKey: "main" }),
|
|
promptForEstimate: "continue",
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(requireCompactEmbeddedAgentSessionCall().currentTokenCount).toBeGreaterThanOrEqual(
|
|
80_000,
|
|
);
|
|
});
|
|
|
|
it("does not add unavailable output twice when full-message estimation already includes it", async () => {
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "x".repeat(3_600),
|
|
usage: {
|
|
input: 1,
|
|
output: 200,
|
|
totalTokens: 201,
|
|
contextUsage: { state: "unavailable" },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 0,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({ sessionId: "session", sessionKey: "main" }),
|
|
promptForEstimate: "",
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 1_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reads flush usage and byte size from SQLite without statting a retired transcript path", async () => {
|
|
const sessionFile = path.join(rootDir, "memory-flush-usage-and-size.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "large answer",
|
|
usage: { input: 80_000, output: 4_000 },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const originalStat = fsCore.promises.stat.bind(fsCore.promises);
|
|
const statSpy = vi
|
|
.spyOn(fsCore.promises, "stat")
|
|
.mockImplementation(async (target, options) => originalStat(target, options));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
let directTranscriptStats: unknown[];
|
|
try {
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
directTranscriptStats = statSpy.mock.calls.filter(
|
|
([target]) => String(target) === sessionFile,
|
|
);
|
|
} finally {
|
|
statSpy.mockRestore();
|
|
}
|
|
|
|
expect(directTranscriptStats).toEqual([]);
|
|
expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("fails when required preflight compaction returns an unknown successful no-op", async () => {
|
|
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
|
ok: true,
|
|
compacted: false,
|
|
reason: "plugin already stored this turn",
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 180_499,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const sessionStore = { main: sessionEntry };
|
|
const replyOperation = createReplyOperation();
|
|
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 200_000,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
}),
|
|
).rejects.toThrow("Preflight compaction required but failed: plugin already stored this turn");
|
|
|
|
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.contextTokenBudget).toBe(200_000);
|
|
expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting");
|
|
expect(replyOperation.updateSessionId).not.toHaveBeenCalled();
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
expect(refreshQueuedFollowupSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips OpenClaw preflight compaction for explicit Codex runtime overrides", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 347_000,
|
|
totalTokensFresh: false,
|
|
agentRuntimeOverride: "codex",
|
|
agentHarnessId: "openclaw",
|
|
};
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
models: {
|
|
providers: {
|
|
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
|
|
},
|
|
},
|
|
agents: { defaults: { compaction: { memoryFlush: {} } } },
|
|
} as never,
|
|
followupRun: createTestFollowupRun({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
sessionId: "session",
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "gpt-5.5",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips fresh persisted token totals for explicit Codex runtime overrides", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 347_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
agentRuntimeOverride: "codex",
|
|
agentHarnessId: "openclaw",
|
|
};
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
models: {
|
|
providers: {
|
|
openai: { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
|
|
},
|
|
},
|
|
agents: { defaults: { compaction: { memoryFlush: {} } } },
|
|
} as never,
|
|
followupRun: createTestFollowupRun({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
sessionId: "session",
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "gpt-5.5",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips preflight compaction for compatible CLI session runtime pins", async () => {
|
|
registerClaudeCliBackend();
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 347_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
agentRuntimeOverride: "claude-cli",
|
|
};
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
models: {
|
|
providers: {
|
|
anthropic: { models: [{ id: "claude-opus-4-6", contextWindow: 350_000 }] },
|
|
},
|
|
},
|
|
agents: { defaults: { compaction: { memoryFlush: {} } } },
|
|
} as never,
|
|
followupRun: createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-6",
|
|
sessionId: "session",
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps the OpenAI API context window for persisted OpenClaw runtime overrides", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 347_000,
|
|
totalTokensFresh: false,
|
|
agentRuntimeOverride: "openclaw",
|
|
};
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
models: {
|
|
providers: {
|
|
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
|
|
},
|
|
},
|
|
agents: { defaults: { compaction: { memoryFlush: {} } } },
|
|
} as never,
|
|
followupRun: createTestFollowupRun({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
sessionId: "session",
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "gpt-5.5",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not use the active run sessionFile when the session entry has no transcript path", async () => {
|
|
const sessionFile = path.join(rootDir, "active-run-session.jsonl");
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
`${JSON.stringify({
|
|
message: {
|
|
role: "assistant",
|
|
content: "large answer",
|
|
usage: { input: 90_000, output: 8_000 },
|
|
},
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not treat unavailable Anthropic context as transcript prompt usage", async () => {
|
|
const sessionFile = path.join(rootDir, "unavailable-context-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "small answer",
|
|
usage: {
|
|
input: 12,
|
|
output: 15_104,
|
|
cacheRead: 819_661,
|
|
cacheWrite: 93_130,
|
|
contextUsage: { state: "unavailable" },
|
|
totalTokens: 927_907,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps preflight compaction conservative for content appended after latest usage", async () => {
|
|
const sessionFile = path.join(rootDir, "post-usage-tail-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "small answer",
|
|
usage: { input: 40_000, output: 2_000 },
|
|
},
|
|
},
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "tool",
|
|
content: `large interrupted tool output ${"x".repeat(450_000)}`,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.currentTokenCount).toBeGreaterThan(100_000);
|
|
});
|
|
|
|
it("combines latest usage with post-usage tail pressure for preflight compaction", async () => {
|
|
const sessionFile = path.join(rootDir, "combined-tail-pressure-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "small answer",
|
|
usage: { input: 86_000, output: 2_000 },
|
|
},
|
|
},
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "tool",
|
|
content: `moderate interrupted tool output ${"x".repeat(36_000)}`,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.currentTokenCount).toBeGreaterThanOrEqual(96_000);
|
|
});
|
|
|
|
it("does not count bytes from a large latest usage record as post-usage tail pressure", async () => {
|
|
const sessionFile = path.join(rootDir, "large-usage-record-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: `large answer ${"x".repeat(300_000)}`,
|
|
usage: { input: 40_000, output: 2_000 },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not treat raw transcript metadata bytes as token pressure", async () => {
|
|
const sessionFile = path.join(rootDir, "metadata-heavy-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [
|
|
{
|
|
type: "custom",
|
|
payload: "x".repeat(450_000),
|
|
},
|
|
{
|
|
type: "message",
|
|
message: {
|
|
role: "assistant",
|
|
content: "small answer",
|
|
usage: { input: 40_000, output: 2_000 },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
const originalStat = fsCore.promises.stat.bind(fsCore.promises);
|
|
const statSpy = vi
|
|
.spyOn(fsCore.promises, "stat")
|
|
.mockImplementation(async (target, options) => originalStat(target, options));
|
|
|
|
let entry: SessionEntry | undefined;
|
|
let directTranscriptStats: unknown[];
|
|
try {
|
|
entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
memoryFlush: {},
|
|
maxActiveTranscriptBytes: "10mb",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
directTranscriptStats = statSpy.mock.calls.filter(
|
|
([target]) => String(target) === sessionFile,
|
|
);
|
|
} finally {
|
|
statSpy.mockRestore();
|
|
}
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
expect(directTranscriptStats).toEqual([]);
|
|
});
|
|
|
|
it("triggers preflight compaction when the active transcript exceeds the configured byte threshold", async () => {
|
|
const sessionFile = path.join(rootDir, "large-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [{ type: "message", message: { role: "user", content: "x".repeat(256) } }],
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 10,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const sessionStore = { main: sessionEntry };
|
|
const replyOperation = createReplyOperation();
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
maxActiveTranscriptBytes: "10b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(entry?.compactionCount).toBe(1);
|
|
expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting");
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.sessionId).toBe("session");
|
|
expect(compactCall.trigger).toBe("budget");
|
|
expect(compactCall.currentTokenCount).toBe(12);
|
|
expect(compactCall.sessionFile).toBe("main");
|
|
});
|
|
|
|
it("byte-guards a Codex runtime session through SQLite semantic compaction", async () => {
|
|
const storePath = path.join(rootDir, "sqlite-codex-byte-guard.json");
|
|
const sessionKey = "agent:main:main";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
{ message: { role: "user", content: "x".repeat(256) }, type: "message" },
|
|
]);
|
|
expect(readTranscriptStatsSync(scope).sizeBytes).toBeGreaterThan(10);
|
|
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 10,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
agentRuntimeOverride: "codex",
|
|
agentHarnessId: "openclaw",
|
|
};
|
|
const sessionStore = { [sessionKey]: sessionEntry };
|
|
const replyOperation = createReplyOperation();
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { maxActiveTranscriptBytes: "10b" },
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
sessionId: "session",
|
|
sessionKey,
|
|
}),
|
|
defaultModel: "gpt-5.5",
|
|
agentCfgContextTokens: 1_000_000,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(entry?.compactionCount).toBe(1);
|
|
expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting");
|
|
expect(requireCompactEmbeddedAgentSessionCall()).toMatchObject({
|
|
agentHarnessId: "openclaw",
|
|
deferOwningContextEngineCompaction: false,
|
|
preflightCompactionTrigger: "transcript_bytes",
|
|
preflightRequired: true,
|
|
sessionId: "session",
|
|
sessionKey,
|
|
trigger: "budget",
|
|
});
|
|
});
|
|
|
|
it("leaves an under-limit SQLite Codex session to native token compaction", async () => {
|
|
const storePath = path.join(rootDir, "sqlite-codex-under-byte-guard.json");
|
|
const sessionKey = "agent:main:main";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
{ message: { role: "user", content: "small" }, type: "message" },
|
|
]);
|
|
expect(readTranscriptStatsSync(scope).sizeBytes).toBeLessThan(10 * 1024);
|
|
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 347_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
agentRuntimeOverride: "codex",
|
|
agentHarnessId: "openclaw",
|
|
};
|
|
const replyOperation = createReplyOperation();
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: { maxActiveTranscriptBytes: "10kb" },
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
sessionId: "session",
|
|
sessionKey,
|
|
}),
|
|
defaultModel: "gpt-5.5",
|
|
agentCfgContextTokens: 350_000,
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(entry).toBe(sessionEntry);
|
|
expect(replyOperation.setPhase).not.toHaveBeenCalled();
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps ownsNativeCompaction absolute over the SQLite transcript byte guard", async () => {
|
|
registerClaudeCliBackend(true);
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 10,
|
|
reserveTokensFloor: 20_000,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const storePath = path.join(rootDir, "sqlite-cli-owned-session.json");
|
|
const sessionKey = "agent:main:main";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
{ message: { role: "user", content: "x".repeat(256) }, type: "message" },
|
|
]);
|
|
expect(readTranscriptStatsSync(scope).sizeBytes).toBeGreaterThan(10);
|
|
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 10,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } },
|
|
},
|
|
compaction: {
|
|
memoryFlush: {},
|
|
maxActiveTranscriptBytes: "10b",
|
|
},
|
|
},
|
|
},
|
|
} as const;
|
|
const followupRun = createTestFollowupRun({
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-6",
|
|
sessionId: "session",
|
|
sessionKey,
|
|
});
|
|
|
|
const flushResult = await runMemoryFlushIfNeeded({
|
|
cfg,
|
|
followupRun,
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
const preflightEntry = await runPreflightCompactionIfNeeded({
|
|
cfg,
|
|
followupRun,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(flushResult).toEqual({ sessionEntry, outcome: "skipped" });
|
|
expect(preflightEntry).toBe(sessionEntry);
|
|
expect(preflightEntry?.compactionCount).toBe(0);
|
|
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still byte-guards a genuine embedded SQLite-backed session", async () => {
|
|
const storePath = path.join(rootDir, "sqlite-large-session.json");
|
|
const sessionKey = "agent:main:main";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
{ message: { role: "user", content: "x".repeat(256) }, type: "message" },
|
|
]);
|
|
expect(readTranscriptStatsSync(scope).sizeBytes).toBeGreaterThan(10);
|
|
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 10,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const replyOperation = createReplyOperation();
|
|
|
|
const entry = await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
maxActiveTranscriptBytes: "10b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionKey,
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(entry?.compactionCount).toBe(1);
|
|
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
|
expect(compactCall.trigger).toBe("budget");
|
|
expect(compactCall.preflightCompactionTrigger).toBe("transcript_bytes");
|
|
});
|
|
|
|
it("keeps incognito preflight compaction in the process-local transcript store", async () => {
|
|
const durableStorePath = path.join(rootDir, "durable-sessions.json");
|
|
const sessionKey = "agent:main:dashboard:incognito-preflight";
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "incognito-session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 90_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: sessionEntry.sessionId,
|
|
sessionKey,
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath: durableStorePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const expectedStorePath = resolveSessionStorePathForScope({
|
|
agentId: "main",
|
|
sessionKey,
|
|
storePath: durableStorePath,
|
|
});
|
|
expect(
|
|
(requireCompactEmbeddedAgentSessionCall() as { sessionTarget?: Record<string, unknown> })
|
|
.sessionTarget,
|
|
).toMatchObject({
|
|
agentId: "main",
|
|
sessionId: sessionEntry.sessionId,
|
|
sessionKey,
|
|
storePath: expectedStorePath,
|
|
});
|
|
expect(incrementCompactionCountMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ agentId: "main", sessionKey, storePath: expectedStorePath }),
|
|
);
|
|
});
|
|
|
|
it("resolves usage from an active branch whose leaf target predates the bounded tail", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 0,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const storePath = path.join(rootDir, "sqlite-deep-leaf-session.json");
|
|
const sessionKey = "agent:main:deep-leaf";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
const activeRoot = {
|
|
type: "message",
|
|
id: "active-root",
|
|
parentId: null,
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
message: {
|
|
role: "assistant",
|
|
content: "active",
|
|
usage: { input: 10, output: 5 },
|
|
},
|
|
};
|
|
let parentId = activeRoot.id;
|
|
const abandonedBranch = Array.from({ length: 512 }, (_, index) => {
|
|
const id = `abandoned-${index}`;
|
|
const event = {
|
|
type: "message",
|
|
id,
|
|
parentId,
|
|
timestamp: `2026-01-01T00:00:${String(index % 60).padStart(2, "0")}.000Z`,
|
|
message: {
|
|
role: "assistant",
|
|
content: "abandoned",
|
|
usage: { input: 90_000, output: 10_000 },
|
|
},
|
|
};
|
|
parentId = id;
|
|
return event;
|
|
});
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
activeRoot,
|
|
...abandonedBranch,
|
|
{
|
|
type: "leaf",
|
|
id: "return-to-active-root",
|
|
parentId,
|
|
targetId: activeRoot.id,
|
|
appendParentId: activeRoot.id,
|
|
timestamp: "2026-01-01T00:01:00.000Z",
|
|
},
|
|
]);
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokensFresh: false,
|
|
};
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({ sessionId: "session", sessionKey }),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("forces memory flush when a SQLite-backed transcript exceeds the byte threshold", async () => {
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 10,
|
|
reserveTokensFloor: 20_000,
|
|
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
|
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
const storePath = path.join(rootDir, "sqlite-force-flush-session.json");
|
|
const sessionKey = "agent:main:main";
|
|
const scope = { agentId: "main", sessionId: "session", sessionKey, storePath };
|
|
await upsertSessionEntry(scope, { sessionId: "session", updatedAt: 10 });
|
|
await replaceSqliteTranscriptEvents(scope, [
|
|
{ message: { role: "user", content: "x".repeat(256) }, type: "message" },
|
|
]);
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 10,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const replyOperation = createReplyOperation();
|
|
|
|
const result = await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({ sessionId: "session", sessionKey }),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { [sessionKey]: sessionEntry },
|
|
sessionKey,
|
|
storePath,
|
|
isHeartbeat: false,
|
|
replyOperation,
|
|
});
|
|
|
|
expect(result.outcome).toBe("completed");
|
|
expect(replyOperation.setPhase).toHaveBeenCalledWith("memory_flushing");
|
|
expect(runEmbeddedAgentMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("emits preflight compaction notices around a successful budget compaction", async () => {
|
|
const sessionFile = path.join(rootDir, "notify-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [{ type: "message", message: { role: "user", content: "x".repeat(5_000) } }],
|
|
});
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const onCompactionNotice = vi.fn();
|
|
|
|
await runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
notifyUser: true,
|
|
maxActiveTranscriptBytes: "10b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onCompactionNotice,
|
|
});
|
|
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(1, "start");
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(2, "end");
|
|
});
|
|
|
|
it("emits an incomplete preflight compaction notice when post-compaction state update throws", async () => {
|
|
const sessionFile = path.join(rootDir, "notify-failed-session.jsonl");
|
|
await writeTestSessionTranscript({
|
|
rootDir,
|
|
events: [{ type: "message", message: { role: "user", content: "x".repeat(5_000) } }],
|
|
});
|
|
incrementCompactionCountMock.mockRejectedValueOnce(new Error("count update failed"));
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 120,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 0,
|
|
};
|
|
const onCompactionNotice = vi.fn();
|
|
|
|
await expect(
|
|
runPreflightCompactionIfNeeded({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
compaction: {
|
|
notifyUser: true,
|
|
maxActiveTranscriptBytes: "10b",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
followupRun: createTestFollowupRun({
|
|
sessionId: "session",
|
|
sessionFile,
|
|
sessionKey: "main",
|
|
}),
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
storePath: path.join(rootDir, "sessions.json"),
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
onCompactionNotice,
|
|
}),
|
|
).rejects.toThrow("count update failed");
|
|
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(1, "start");
|
|
expect(onCompactionNotice).toHaveBeenNthCalledWith(2, "incomplete");
|
|
});
|
|
|
|
it("uses configured prompts and stored bootstrap warning signatures", async () => {
|
|
const sessionEntry: SessionEntry = {
|
|
sessionId: "session",
|
|
updatedAt: Date.now(),
|
|
totalTokens: 80_000,
|
|
totalTokensFresh: true,
|
|
totalTokensVersion: 1,
|
|
compactionCount: 1,
|
|
systemPromptReport: {
|
|
source: "run",
|
|
generatedAt: Date.now(),
|
|
systemPrompt: { chars: 1, projectContextChars: 0, nonProjectContextChars: 1 },
|
|
injectedWorkspaceFiles: [],
|
|
skills: { promptChars: 0, entries: [] },
|
|
tools: { listChars: 0, schemaChars: 0, entries: [] },
|
|
bootstrapTruncation: {
|
|
warningMode: "once",
|
|
warningShown: true,
|
|
promptWarningSignature: "sig-b",
|
|
warningSignaturesSeen: ["sig-a", "sig-b"],
|
|
truncatedFiles: 1,
|
|
nearLimitFiles: 0,
|
|
totalNearLimit: false,
|
|
},
|
|
},
|
|
};
|
|
registerMemoryFlushPlanResolverForTest(() => ({
|
|
softThresholdTokens: 4_000,
|
|
forceFlushTranscriptBytes: 1_000_000_000,
|
|
reserveTokensFloor: 20_000,
|
|
prompt: "Write notes.\nNO_REPLY to memory/2023-11-14.md and MEMORY.md",
|
|
systemPrompt: "Flush memory now. NO_REPLY memory/YYYY-MM-DD.md MEMORY.md",
|
|
relativePath: "memory/2023-11-14.md",
|
|
}));
|
|
|
|
await runMemoryFlushIfNeeded({
|
|
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
|
followupRun: createTestFollowupRun({ extraSystemPrompt: "extra system" }),
|
|
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
|
defaultModel: "anthropic/claude-opus-4-6",
|
|
agentCfgContextTokens: 100_000,
|
|
resolvedVerboseLevel: "off",
|
|
sessionEntry,
|
|
sessionStore: { main: sessionEntry },
|
|
sessionKey: "main",
|
|
isHeartbeat: false,
|
|
replyOperation: createReplyOperation(),
|
|
});
|
|
|
|
const flushCall = requireEmbeddedAgentCall();
|
|
expect(flushCall.prompt).toContain("Write notes.");
|
|
expect(flushCall.prompt).toContain("NO_REPLY");
|
|
expect(flushCall.prompt).toContain("MEMORY.md");
|
|
expect(flushCall.transcriptPrompt).toBe("");
|
|
expect(flushCall.extraSystemPrompt).toContain("extra system");
|
|
expect(flushCall.extraSystemPrompt).toContain("Flush memory now.");
|
|
expect(flushCall.memoryFlushWritePath).toBe("memory/2023-11-14.md");
|
|
expect(flushCall.silentExpected).toBe(true);
|
|
expect(flushCall.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]);
|
|
expect(flushCall.bootstrapPromptWarningSignature).toBe("sig-b");
|
|
});
|
|
});
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|