mirror of https://github.com/openclaw/openclaw.git
fix(compaction): make compaction guard content-aware to prevent false cancellations in heartbeat sessions (#42119)
Merged via squash.
Prepared head SHA: 3429643315
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
parent
ef7a5c3546
commit
5c05347d11
|
|
@ -410,6 +410,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
- Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec.
|
||||
- Plugins/context engines: retry legacy lifecycle calls once without `sessionKey` when older plugins reject that field, memoize legacy mode after the first strict-schema fallback, and preserve non-compat runtime errors without retry. (#44779) thanks @hhhhao28.
|
||||
- Agents/compaction: treat markup-wrapped heartbeat boilerplate as non-meaningful session history when deciding whether to compact, so heartbeat-only sessions no longer keep compaction alive due to wrapper formatting. (#42119) thanks @samzong.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { stripHeartbeatToken } from "../auto-reply/heartbeat.js";
|
||||
import { isSilentReplyText } from "../auto-reply/tokens.js";
|
||||
|
||||
export const TOOL_RESULT_REAL_CONVERSATION_LOOKBACK = 20;
|
||||
const NON_CONVERSATION_BLOCK_TYPES = new Set([
|
||||
"toolCall",
|
||||
"toolUse",
|
||||
"functionCall",
|
||||
"thinking",
|
||||
"reasoning",
|
||||
]);
|
||||
|
||||
function hasMeaningfulText(text: string): boolean {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (isSilentReplyText(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
const heartbeat = stripHeartbeatToken(trimmed, { mode: "message" });
|
||||
if (heartbeat.didStrip) {
|
||||
return heartbeat.text.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function hasMeaningfulConversationContent(message: AgentMessage): boolean {
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
return hasMeaningfulText(content);
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return false;
|
||||
}
|
||||
let sawMeaningfulNonTextBlock = false;
|
||||
for (const block of content) {
|
||||
if (!block || typeof block !== "object") {
|
||||
continue;
|
||||
}
|
||||
const type = (block as { type?: unknown }).type;
|
||||
if (type !== "text") {
|
||||
// Tool-call metadata and internal reasoning blocks do not make a
|
||||
// heartbeat-only transcript count as real conversation.
|
||||
if (typeof type === "string" && NON_CONVERSATION_BLOCK_TYPES.has(type)) {
|
||||
continue;
|
||||
}
|
||||
sawMeaningfulNonTextBlock = true;
|
||||
continue;
|
||||
}
|
||||
const text = (block as { text?: unknown }).text;
|
||||
if (typeof text !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (hasMeaningfulText(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return sawMeaningfulNonTextBlock;
|
||||
}
|
||||
|
||||
export function isRealConversationMessage(
|
||||
message: AgentMessage,
|
||||
messages: AgentMessage[],
|
||||
index: number,
|
||||
): boolean {
|
||||
if (message.role === "user" || message.role === "assistant") {
|
||||
return hasMeaningfulConversationContent(message);
|
||||
}
|
||||
if (message.role !== "toolResult") {
|
||||
return false;
|
||||
}
|
||||
const start = Math.max(0, index - TOOL_RESULT_REAL_CONVERSATION_LOOKBACK);
|
||||
for (let i = index - 1; i >= start; i -= 1) {
|
||||
const candidate = messages[i];
|
||||
if (!candidate || candidate.role !== "user") {
|
||||
continue;
|
||||
}
|
||||
if (hasMeaningfulConversationContent(candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -67,6 +67,18 @@ export const resolveMemorySearchConfigMock = vi.fn(() => ({
|
|||
}));
|
||||
export const resolveSessionAgentIdMock = vi.fn(() => "main");
|
||||
export const estimateTokensMock = vi.fn((_message?: unknown) => 10);
|
||||
export const sessionMessages: unknown[] = [
|
||||
{ role: "user", content: "hello", timestamp: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 2 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "output" }],
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
},
|
||||
];
|
||||
export const sessionAbortCompactionMock: Mock<(reason?: unknown) => void> = vi.fn();
|
||||
export const createOpenClawCodingToolsMock = vi.fn(() => []);
|
||||
|
||||
|
|
@ -134,6 +146,20 @@ export function resetCompactHooksHarnessMocks(): void {
|
|||
resolveSessionAgentIdMock.mockReturnValue("main");
|
||||
estimateTokensMock.mockReset();
|
||||
estimateTokensMock.mockReturnValue(10);
|
||||
sessionMessages.splice(
|
||||
0,
|
||||
sessionMessages.length,
|
||||
{ role: "user", content: "hello", timestamp: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 2 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "output" }],
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
},
|
||||
);
|
||||
sessionAbortCompactionMock.mockReset();
|
||||
createOpenClawCodingToolsMock.mockReset();
|
||||
createOpenClawCodingToolsMock.mockReturnValue([]);
|
||||
|
|
@ -142,6 +168,7 @@ export function resetCompactHooksHarnessMocks(): void {
|
|||
export async function loadCompactHooksHarness(): Promise<{
|
||||
compactEmbeddedPiSessionDirect: typeof import("./compact.js").compactEmbeddedPiSessionDirect;
|
||||
compactEmbeddedPiSession: typeof import("./compact.js").compactEmbeddedPiSession;
|
||||
__testing: typeof import("./compact.js").__testing;
|
||||
onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate;
|
||||
}> {
|
||||
resetCompactHooksHarnessMocks();
|
||||
|
|
@ -176,18 +203,11 @@ export async function loadCompactHooksHarness(): Promise<{
|
|||
createAgentSession: vi.fn(async () => {
|
||||
const session = {
|
||||
sessionId: "session-1",
|
||||
messages: [
|
||||
{ role: "user", content: "hello", timestamp: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 2 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "output" }],
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
},
|
||||
],
|
||||
messages: sessionMessages.map((message) =>
|
||||
typeof structuredClone === "function"
|
||||
? structuredClone(message)
|
||||
: JSON.parse(JSON.stringify(message)),
|
||||
),
|
||||
agent: {
|
||||
replaceMessages: vi.fn((messages: unknown[]) => {
|
||||
session.messages = [...(messages as typeof session.messages)];
|
||||
|
|
@ -358,10 +378,15 @@ export async function loadCompactHooksHarness(): Promise<{
|
|||
resolveChannelCapabilities: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../../utils/message-channel.js", () => ({
|
||||
INTERNAL_MESSAGE_CHANNEL: "webchat",
|
||||
normalizeMessageChannel: vi.fn(() => undefined),
|
||||
}));
|
||||
vi.doMock("../../utils/message-channel.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../utils/message-channel.js")>(
|
||||
"../../utils/message-channel.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
normalizeMessageChannel: vi.fn(() => undefined),
|
||||
};
|
||||
});
|
||||
|
||||
vi.doMock("../pi-embedded-helpers.js", () => ({
|
||||
ensureSessionHeader: vi.fn(async () => {}),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { getApiProvider, unregisterApiProviders } from "@mariozechner/pi-ai";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getCustomApiRegistrySourceId } from "../custom-api-registry.js";
|
||||
|
|
@ -16,12 +17,14 @@ import {
|
|||
resetCompactHooksHarnessMocks,
|
||||
sanitizeSessionHistoryMock,
|
||||
sessionAbortCompactionMock,
|
||||
sessionMessages,
|
||||
sessionCompactImpl,
|
||||
triggerInternalHook,
|
||||
} from "./compact.hooks.harness.js";
|
||||
|
||||
let compactEmbeddedPiSessionDirect: typeof import("./compact.js").compactEmbeddedPiSessionDirect;
|
||||
let compactEmbeddedPiSession: typeof import("./compact.js").compactEmbeddedPiSession;
|
||||
let compactTesting: typeof import("./compact.js").__testing;
|
||||
let onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate;
|
||||
|
||||
const TEST_SESSION_ID = "session-1";
|
||||
|
|
@ -108,6 +111,7 @@ beforeAll(async () => {
|
|||
const loaded = await loadCompactHooksHarness();
|
||||
compactEmbeddedPiSessionDirect = loaded.compactEmbeddedPiSessionDirect;
|
||||
compactEmbeddedPiSession = loaded.compactEmbeddedPiSession;
|
||||
compactTesting = loaded.__testing;
|
||||
onSessionTranscriptUpdate = loaded.onSessionTranscriptUpdate;
|
||||
});
|
||||
|
||||
|
|
@ -154,6 +158,20 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
|||
estimateTokensMock.mockReset();
|
||||
estimateTokensMock.mockReturnValue(10);
|
||||
sessionAbortCompactionMock.mockReset();
|
||||
sessionMessages.splice(
|
||||
0,
|
||||
sessionMessages.length,
|
||||
{ role: "user", content: "hello", timestamp: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 2 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "output" }],
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
},
|
||||
);
|
||||
unregisterApiProviders(getCustomApiRegistrySourceId("ollama"));
|
||||
});
|
||||
|
||||
|
|
@ -490,6 +508,111 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("skips compaction when the transcript only contains boilerplate replies and tool output", async () => {
|
||||
sessionMessages.splice(
|
||||
0,
|
||||
sessionMessages.length,
|
||||
{ role: "user", content: "<b>HEARTBEAT_OK</b>", timestamp: 1 },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "checked" }],
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await compactEmbeddedPiSessionDirect({
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
customInstructions: "focus on decisions",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
compacted: false,
|
||||
reason: "no real conversation messages",
|
||||
});
|
||||
expect(sessionCompactImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips compaction when the transcript only contains heartbeat boilerplate and reasoning blocks", async () => {
|
||||
sessionMessages.splice(
|
||||
0,
|
||||
sessionMessages.length,
|
||||
{ role: "user", content: "<b>HEARTBEAT_OK</b>", timestamp: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "checking" }],
|
||||
timestamp: 2,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await compactEmbeddedPiSessionDirect({
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
customInstructions: "focus on decisions",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
compacted: false,
|
||||
reason: "no real conversation messages",
|
||||
});
|
||||
expect(sessionCompactImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat assistant-only tool-call blocks as meaningful conversation", () => {
|
||||
expect(
|
||||
compactTesting.hasMeaningfulConversationContent({
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
|
||||
} as AgentMessage),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("counts tool output as real only when a meaningful user ask exists in the lookback window", () => {
|
||||
const heartbeatToolResultWindow = [
|
||||
{ role: "user", content: "<b>HEARTBEAT_OK</b>" },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "checked" }],
|
||||
},
|
||||
] as AgentMessage[];
|
||||
expect(
|
||||
compactTesting.hasRealConversationContent(
|
||||
heartbeatToolResultWindow[1],
|
||||
heartbeatToolResultWindow,
|
||||
1,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const realAskToolResultWindow = [
|
||||
{ role: "assistant", content: "NO_REPLY" },
|
||||
{ role: "user", content: "please inspect the failing PR" },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t2",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "checked" }],
|
||||
},
|
||||
] as AgentMessage[];
|
||||
expect(
|
||||
compactTesting.hasRealConversationContent(
|
||||
realAskToolResultWindow[2],
|
||||
realAskToolResultWindow,
|
||||
2,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("registers the Ollama api provider before compaction", async () => {
|
||||
resolveModelMock.mockReturnValue({
|
||||
model: {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ import { resolveSessionAgentId, resolveSessionAgentIds } from "../agent-scope.js
|
|||
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
|
||||
import { listChannelSupportedActions, resolveChannelMessageToolHints } from "../channel-tools.js";
|
||||
import {
|
||||
hasMeaningfulConversationContent,
|
||||
isRealConversationMessage,
|
||||
} from "../compaction-real-conversation.js";
|
||||
import { resolveContextWindowInfo } from "../context-window-guard.js";
|
||||
import { ensureCustomApiRegistered } from "../custom-api-registry.js";
|
||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
||||
|
|
@ -169,8 +173,12 @@ type CompactionMessageMetrics = {
|
|||
contributors: Array<{ role: string; chars: number; tool?: string }>;
|
||||
};
|
||||
|
||||
function hasRealConversationContent(msg: AgentMessage): boolean {
|
||||
return msg.role === "user" || msg.role === "assistant" || msg.role === "toolResult";
|
||||
function hasRealConversationContent(
|
||||
msg: AgentMessage,
|
||||
messages: AgentMessage[],
|
||||
index: number,
|
||||
): boolean {
|
||||
return isRealConversationMessage(msg, messages, index);
|
||||
}
|
||||
|
||||
function createCompactionDiagId(): string {
|
||||
|
|
@ -962,7 +970,11 @@ export async function compactEmbeddedPiSessionDirect(
|
|||
);
|
||||
}
|
||||
|
||||
if (!session.messages.some(hasRealConversationContent)) {
|
||||
if (
|
||||
!session.messages.some((message, index, messages) =>
|
||||
hasRealConversationContent(message, messages, index),
|
||||
)
|
||||
) {
|
||||
log.info(
|
||||
`[compaction] skipping — no real conversation messages (sessionKey=${params.sessionKey ?? params.sessionId})`,
|
||||
);
|
||||
|
|
@ -1281,3 +1293,8 @@ export async function compactEmbeddedPiSession(
|
|||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
hasRealConversationContent,
|
||||
hasMeaningfulConversationContent,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1795,6 +1795,86 @@ describe("compaction-safeguard double-compaction guard", () => {
|
|||
expect(result).toEqual({ cancel: true });
|
||||
expect(getApiKeyMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats tool results as real conversation only when linked to a meaningful user ask", async () => {
|
||||
expect(
|
||||
__testing.isRealConversationMessage(
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "done" }],
|
||||
} as AgentMessage,
|
||||
[
|
||||
{ role: "user", content: "<b>HEARTBEAT_OK</b>" } as AgentMessage,
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t1",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "done" }],
|
||||
} as AgentMessage,
|
||||
],
|
||||
1,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
__testing.isRealConversationMessage(
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t2",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "done" }],
|
||||
} as AgentMessage,
|
||||
[
|
||||
{ role: "user", content: "please inspect the repo" } as AgentMessage,
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "t2",
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "done" }],
|
||||
} as AgentMessage,
|
||||
],
|
||||
1,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat assistant-only tool calls as meaningful conversation", () => {
|
||||
expect(
|
||||
__testing.hasMeaningfulConversationContent({
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
|
||||
} as AgentMessage),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat reasoning-only assistant blocks as meaningful conversation", () => {
|
||||
expect(
|
||||
__testing.hasMeaningfulConversationContent({
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "checking" }],
|
||||
} as AgentMessage),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
__testing.hasMeaningfulConversationContent({
|
||||
role: "assistant",
|
||||
content: [{ type: "reasoning", summary: [] }],
|
||||
} as unknown as AgentMessage),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("treats markup-wrapped heartbeat tokens as boilerplate", () => {
|
||||
expect(
|
||||
__testing.hasMeaningfulConversationContent(
|
||||
castAgentMessage({
|
||||
role: "assistant",
|
||||
content: "<b>HEARTBEAT_OK</b>",
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
async function expectWorkspaceSummaryEmptyForAgentsAlias(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import { extractSections } from "../../auto-reply/reply/post-compaction-context.
|
|||
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { extractKeywords, isQueryStopWordToken } from "../../memory/query-expansion.js";
|
||||
import {
|
||||
hasMeaningfulConversationContent,
|
||||
isRealConversationMessage,
|
||||
} from "../compaction-real-conversation.js";
|
||||
import {
|
||||
BASE_CHUNK_RATIO,
|
||||
type CompactionSummarizationInstructions,
|
||||
|
|
@ -183,10 +187,6 @@ function formatToolFailuresSection(failures: ToolFailure[]): string {
|
|||
return `\n\n## Tool Failures\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
function isRealConversationMessage(message: AgentMessage): boolean {
|
||||
return message.role === "user" || message.role === "assistant" || message.role === "toolResult";
|
||||
}
|
||||
|
||||
function computeFileLists(fileOps: FileOperations): {
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
|
|
@ -774,8 +774,12 @@ async function readWorkspaceContextForSummary(): Promise<string> {
|
|||
export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
api.on("session_before_compact", async (event, ctx) => {
|
||||
const { preparation, customInstructions: eventInstructions, signal } = event;
|
||||
const hasRealSummarizable = preparation.messagesToSummarize.some(isRealConversationMessage);
|
||||
const hasRealTurnPrefix = preparation.turnPrefixMessages.some(isRealConversationMessage);
|
||||
const hasRealSummarizable = preparation.messagesToSummarize.some((message, index, messages) =>
|
||||
isRealConversationMessage(message, messages, index),
|
||||
);
|
||||
const hasRealTurnPrefix = preparation.turnPrefixMessages.some((message, index, messages) =>
|
||||
isRealConversationMessage(message, messages, index),
|
||||
);
|
||||
if (!hasRealSummarizable && !hasRealTurnPrefix) {
|
||||
// When there are no summarizable messages AND no real turn-prefix content,
|
||||
// cancelling compaction leaves context unchanged but the SDK re-triggers
|
||||
|
|
@ -1124,6 +1128,8 @@ export const __testing = {
|
|||
computeAdaptiveChunkRatio,
|
||||
isOversizedForSummary,
|
||||
readWorkspaceContextForSummary,
|
||||
hasMeaningfulConversationContent,
|
||||
isRealConversationMessage,
|
||||
BASE_CHUNK_RATIO,
|
||||
MIN_CHUNK_RATIO,
|
||||
SAFETY_MARGIN,
|
||||
|
|
|
|||
Loading…
Reference in New Issue