Add compaction sidecar kill switch

This commit is contained in:
Robin Newhouse
2026-06-26 10:05:06 -07:00
parent 9288570e7c
commit d40334a757
21 changed files with 398 additions and 31 deletions
@@ -327,6 +327,73 @@ describe("createInteractiveSessionRuntime", () => {
expect(runtime.getActiveSessionId()).toBe(sessionId);
});
it("manual compact skips sidecar writes when the sidecar flag is off", async () => {
const sessionId = "sess-active-sidecar-off";
const messages = [
{ id: "u1", role: "user" as const, content: "hello" },
{ id: "a1", role: "assistant" as const, content: "world" },
];
const compactionState = createSessionCompactionState({
sourceMessages: messages,
compactedMessages: [
{ id: "summary", role: "user" as const, content: "summary" },
],
updatedAt: "2026-01-01T00:00:00.000Z",
});
const manager = {
start: vi.fn().mockResolvedValue({
sessionId,
manifest: createManifest(sessionId),
manifestPath: "/tmp/session.json",
messagesPath: "/tmp/session.messages.json",
}),
readMessages: vi.fn().mockResolvedValue(messages),
updateSessionCompactionState: vi.fn(),
stop: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn().mockResolvedValue(undefined),
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
get: vi.fn(),
list: vi.fn(),
delete: vi.fn(),
send: vi.fn(),
getAccumulatedUsage: vi.fn(),
};
createCliCoreMock.mockResolvedValue(manager);
compactInteractiveMessagesMock.mockResolvedValue({
compacted: true,
canonicalMessages: messages,
compactionState,
});
const { createInteractiveSessionRuntime } = await importRuntime();
const runtime = createInteractiveSessionRuntime({
config: createConfig(),
providerSettingsManager: createProviderSettingsManager(),
chatCommandState: createChatCommandState(),
requestToolApproval: vi.fn(),
resolveToolPolicy: () => ({ autoApprove: true }),
askQuestionRef: { current: null },
resolveMistakeLimitDecision: undefined,
switchToActModeTool: {} as never,
onAgentEvent: vi.fn(),
onTeamEvent: vi.fn(),
onPendingPrompts: vi.fn(),
onPendingPromptSubmitted: vi.fn(),
isCompactionSidecarEnabled: () => false,
});
await runtime.ensureReady();
const result = await runtime.compactCurrentSession();
expect(result).toEqual({
messagesBefore: messages.length,
messagesAfter: messages.length,
workingContextMessagesAfter: compactionState.messages.length,
compacted: true,
});
expect(manager.updateSessionCompactionState).not.toHaveBeenCalled();
expect(runtime.getActiveSessionId()).toBe(sessionId);
});
it("rejects manual compact while the active session is running", async () => {
const sessionId = "sess-running";
const messages = [{ role: "user" as const, content: "hello" }];
@@ -3,6 +3,7 @@ import {
type AgentHooks,
type CheckpointEntry,
createSessionCompactionState,
createSessionCompactionSidecarEnabledResolver,
isSessionNotFoundError,
type PendingPromptMutationResult,
type ProviderSettingsManager,
@@ -96,8 +97,12 @@ export function createInteractiveSessionRuntime(input: {
onTeamEvent: (event: TeamEvent) => void;
onPendingPrompts: (event: PendingPromptSnapshot) => void;
onPendingPromptSubmitted: (event: PendingPromptSubmittedEvent) => void;
isCompactionSidecarEnabled?: () => boolean;
}) {
let sessionManager: CliCore | undefined;
const isCompactionSidecarEnabled =
input.isCompactionSidecarEnabled ??
createSessionCompactionSidecarEnabledResolver();
let runtimeHooks: RuntimeHooks | undefined;
let unsubscribeAgent = () => {};
let unsubscribePendingPrompts = () => {};
@@ -210,7 +215,9 @@ export function createInteractiveSessionRuntime(input: {
toolPolicies: input.config.toolPolicies,
interactive: true,
initialMessages: initial,
...(initialCompactionState ? { initialCompactionState } : {}),
...(isCompactionSidecarEnabled() && initialCompactionState
? { initialCompactionState }
: {}),
...(sessionMetadata ? { sessionMetadata } : {}),
localRuntime: {
onTeamRestored: () => {},
@@ -291,6 +298,9 @@ export function createInteractiveSessionRuntime(input: {
const readCompactionState = async (
sessionId: string,
): Promise<SessionCompactionState | undefined> => {
if (!isCompactionSidecarEnabled()) {
return undefined;
}
const manager = sessionManager;
if (!manager) {
return undefined;
@@ -624,6 +634,14 @@ export function createInteractiveSessionRuntime(input: {
compacted: false,
};
}
if (!isCompactionSidecarEnabled()) {
return {
messagesBefore,
messagesAfter: result.canonicalMessages.length,
workingContextMessagesAfter: result.compactionState.messages.length,
compacted: true,
};
}
const updated = await manager.updateSessionCompactionState(
sourceSessionId,
result.compactionState,
+4
View File
@@ -1,5 +1,6 @@
import {
getCurrentContextSize,
createSessionCompactionSidecarEnabledResolver,
type ProviderSettings,
ProviderSettingsManager,
type UserInstructionConfigService,
@@ -25,6 +26,7 @@ import { disableOpenTuiGraphicsProbe } from "../tui/opentui-env";
import type { QueuedPromptItem } from "../tui/types";
import { type ChatCommandState, chatCommandHost } from "../utils/chat-commands";
import { applyCliCompactionMode } from "../utils/compaction-mode";
import { getCliFeatureFlagsService } from "../utils/feature-flags";
import {
shouldZeroClineFreeModelCost,
zeroCliAgentEventCost,
@@ -201,6 +203,8 @@ export async function runInteractive(
onPendingPromptSubmitted: (event) => {
uiEvents.emit("pending-prompt-submitted", event);
},
isCompactionSidecarEnabled:
createSessionCompactionSidecarEnabledResolver(getCliFeatureFlagsService()),
});
let modeChangePromise: Promise<void> | undefined;
let modeChangeTarget: "plan" | "act" | undefined;
+11 -3
View File
@@ -50,6 +50,7 @@ import {
NoOpFeatureFlagsProvider,
} from "./services/feature-flags";
import { resolveCoreDistinctId } from "./services/telemetry/distinct-id";
import { createSessionCompactionSidecarEnabledResolver } from "./session/models/session-compaction";
import type { CoreSessionEvent } from "./types/events";
import type { SessionHistoryRecord } from "./types/sessions";
@@ -202,9 +203,6 @@ export class ClineCore {
static async create(options: ClineCoreOptions = {}): Promise<ClineCore> {
const distinctId = resolveCoreDistinctId(options.distinctId);
const capabilities = normalizeRuntimeCapabilities(options.capabilities);
const normalizedOptions = { ...options, capabilities, distinctId };
const host = await createRuntimeHost(normalizedOptions);
const automationOptions = normalizeAutomationOptions(options.automation);
const featureFlags =
options.featureFlags ||
new FeatureFlagsService({
@@ -216,6 +214,16 @@ export class ClineCore {
clientName: options.clientName,
},
});
const normalizedOptions = {
...options,
capabilities,
distinctId,
isCompactionSidecarEnabled:
options.isCompactionSidecarEnabled ??
createSessionCompactionSidecarEnabledResolver(featureFlags),
};
const host = await createRuntimeHost(normalizedOptions);
const automationOptions = normalizeAutomationOptions(options.automation);
const core = new ClineCore(
host,
options.clientName,
@@ -220,6 +220,11 @@ export interface ClineCoreOptions {
* If omitted, Core uses a no-op provider with default flag values.
*/
featureFlags?: FeatureFlagsService;
/**
* Overrides the compaction sidecar rollout decision.
* @internal
*/
isCompactionSidecarEnabled?: () => boolean;
/**
* Optional structured logger for core-side operational diagnostics such as
* runtime-host selection and fallback decisions.
@@ -654,11 +654,73 @@ describe("HubServerTransport boundaries", () => {
},
});
await expect(answerPromise).resolves.toBe("Use hub");
});
await expect(answerPromise).resolves.toBe("Use hub");
});
it("does not transfer capability ownership to attached clients", async () => {
let createdSessionId = "";
it("ignores initial compaction sidecar state when the sidecar flag is off", async () => {
let capturedStartInput: StartSessionInput | undefined;
const startSession = vi.fn(async (input: StartSessionInput) => {
capturedStartInput = input;
const sessionId = input.config.sessionId?.trim() || "session-1";
return {
sessionId,
manifest: {
version: 1,
session_id: sessionId,
source: "cli",
pid: 1,
started_at: new Date(0).toISOString(),
status: "running",
interactive: true,
provider: "cline",
model: "test-model",
cwd: "/tmp/project",
workspace_root: "/tmp/project",
enable_tools: true,
enable_spawn: true,
enable_teams: false,
},
manifestPath: "",
messagesPath: "",
result: undefined,
};
});
const transport = createTransport({
sessionHost: { startSession },
isCompactionSidecarEnabled: () => false,
});
const initialCompactionState = createSessionCompactionState({
sourceMessages: [{ role: "user", content: "source" }],
compactedMessages: [{ role: "user", content: "summary" }],
conversationId: "session-1",
});
const reply = await transport.handleCommand({
version: "v1",
requestId: "req-create-sidecar-off",
command: "session.create",
clientId: "client-1",
payload: {
workspaceRoot: "/tmp/project",
cwd: "/tmp/project",
sessionConfig: {
sessionId: "session-1",
providerId: "cline",
modelId: "test-model",
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
systemPrompt: "system",
},
initialCompactionState,
},
});
expect(reply.ok).toBe(true);
expect(capturedStartInput?.initialCompactionState).toBeUndefined();
});
it("does not transfer capability ownership to attached clients", async () => {
let createdSessionId = "";
const startSession = vi.fn(async (input: StartSessionInput) => {
createdSessionId = input.config.sessionId?.trim() || "missing-session";
return {
@@ -1071,6 +1133,30 @@ describe("HubServerTransport boundaries", () => {
expect(readSessionCompactionState).toHaveBeenCalledWith("session-1");
});
it("does not read compaction sidecar state when the sidecar flag is off", async () => {
const readSessionCompactionState = vi.fn();
const transport = createTransport({
sessionHost: { readSessionCompactionState },
isCompactionSidecarEnabled: () => false,
});
const ctx = getContext(transport);
ensureSessionState(ctx, "session-1", "owner-client", "creator");
const reply = await transport.handleCommand({
version: "v1",
requestId: "req-compact-get-off",
command: "session.compaction.get",
clientId: "owner-client",
sessionId: "session-1",
});
expect(reply).toMatchObject({
ok: true,
payload: { sessionId: "session-1", disabled: true },
});
expect(readSessionCompactionState).not.toHaveBeenCalled();
});
it("rejects invalid compaction sidecar updates before calling the session host", async () => {
const updateSessionCompactionState = vi.fn();
const transport = createTransport({
@@ -1095,6 +1181,36 @@ describe("HubServerTransport boundaries", () => {
expect(updateSessionCompactionState).not.toHaveBeenCalled();
});
it("does not update compaction sidecar state when the sidecar flag is off", async () => {
const state = createSessionCompactionState({
sourceMessages: [{ role: "user", content: "source" }],
compactedMessages: [{ role: "user", content: "summary" }],
conversationId: "session-1",
});
const updateSessionCompactionState = vi.fn();
const transport = createTransport({
sessionHost: { updateSessionCompactionState },
isCompactionSidecarEnabled: () => false,
});
const ctx = getContext(transport);
ensureSessionState(ctx, "session-1", "owner-client", "creator");
const reply = await transport.handleCommand({
version: "v1",
requestId: "req-compact-update-off",
command: "session.compaction.update",
clientId: "owner-client",
sessionId: "session-1",
payload: { state },
});
expect(reply).toMatchObject({
ok: true,
payload: { sessionId: "session-1", updated: false, disabled: true },
});
expect(updateSessionCompactionState).not.toHaveBeenCalled();
});
it("publishes session updates after successful compaction sidecar updates", async () => {
const state = createSessionCompactionState({
sourceMessages: [{ role: "user", content: "source" }],
@@ -34,6 +34,7 @@ describe("connector hub handlers", () => {
pendingCapabilityRequests: new Map(),
suppressNextTerminalEventBySession: new Map(),
telemetry: telemetry as never,
isCompactionSidecarEnabled: () => true,
sessionHost: {} as never,
publish: vi.fn(),
buildEvent: vi.fn() as never,
@@ -51,6 +51,7 @@ export interface HubTransportContext {
readonly pendingCapabilityRequests: Map<string, PendingCapabilityRequest>;
readonly suppressNextTerminalEventBySession: Map<string, string>;
readonly telemetry?: ITelemetryService;
readonly isCompactionSidecarEnabled: () => boolean;
readonly sessionHost: RuntimeHost &
Partial<PendingPromptsRuntimeService & SessionUsageRuntimeService>;
publish(event: HubEventEnvelope): void;
@@ -14,6 +14,7 @@ function createContext(
pendingApprovals: new Map(),
pendingCapabilityRequests: new Map(),
suppressNextTerminalEventBySession: new Map(),
isCompactionSidecarEnabled: () => true,
sessionHost: {
startSession: vi.fn(),
runTurn: vi.fn(),
@@ -111,9 +111,9 @@ export async function handleSessionCreate(
payload.runtimeOptions && typeof payload.runtimeOptions === "object"
? (payload.runtimeOptions as Record<string, unknown>)
: {};
const initialCompactionState = parseSessionCompactionState(
payload.initialCompactionState,
);
const initialCompactionState = ctx.isCompactionSidecarEnabled()
? parseSessionCompactionState(payload.initialCompactionState)
: undefined;
if (typeof sessionConfig?.mode === "string") {
metadata.mode = sessionConfig.mode;
} else if (typeof runtimeOptions.mode === "string") {
@@ -387,9 +387,9 @@ export async function handleSessionRestore(
payload.runtimeOptions && typeof payload.runtimeOptions === "object"
? (payload.runtimeOptions as Record<string, unknown>)
: {};
const initialCompactionState = parseSessionCompactionState(
payload.initialCompactionState,
);
const initialCompactionState = ctx.isCompactionSidecarEnabled()
? parseSessionCompactionState(payload.initialCompactionState)
: undefined;
const metadata =
payload.metadata && typeof payload.metadata === "object"
? JSON.parse(JSON.stringify(payload.metadata))
@@ -757,6 +757,9 @@ export async function handleSessionCompactionGet(
`Unknown session: ${sessionId}`,
);
}
if (!ctx.isCompactionSidecarEnabled()) {
return okReply(envelope, { sessionId, state: undefined, disabled: true });
}
const clientId = envelope.clientId?.trim() || "hub-client";
const unauthorized = authorizeSessionCompactionAccess({
sessionId,
@@ -841,6 +844,9 @@ export async function handleSessionCompactionUpdate(
`Unknown session: ${sessionId}`,
);
}
if (!ctx.isCompactionSidecarEnabled()) {
return okReply(envelope, { sessionId, updated: false, disabled: true });
}
const unauthorized = authorizeSessionCompactionAccess({
sessionId,
ctx,
@@ -43,6 +43,7 @@ export interface HubWebSocketServerOptions {
* Ignored when `sessionHost` is supplied.
*/
telemetry?: ITelemetryService;
isCompactionSidecarEnabled?: () => boolean;
}
export interface HubWebSocketServer {
@@ -15,6 +15,7 @@ import type {
RuntimeHost,
} from "../../runtime/host/runtime-host";
import { SqliteSessionStore } from "../../services/storage/sqlite-session-store";
import { createSessionCompactionSidecarEnabledResolver } from "../../session/models/session-compaction";
import { CoreSessionService } from "../../session/services/session-service";
import {
type CoreSettingsListInput,
@@ -183,12 +184,16 @@ export class HubServerTransport implements NativeHubTransport {
private readonly ctx: HubTransportContext;
constructor(readonly options: HubWebSocketServerOptions) {
const isCompactionSidecarEnabled =
options.isCompactionSidecarEnabled ??
createSessionCompactionSidecarEnabledResolver();
this.sessionHost =
options.sessionHost ??
new LocalRuntimeHost({
sessionService: new CoreSessionService(new SqliteSessionStore()),
fetch: options.fetch,
telemetry: options.telemetry,
isCompactionSidecarEnabled,
});
this.ctx = {
clients: this.clients,
@@ -198,6 +203,7 @@ export class HubServerTransport implements NativeHubTransport {
suppressNextTerminalEventBySession:
this.suppressNextTerminalEventBySession,
telemetry: options.telemetry,
isCompactionSidecarEnabled,
sessionHost: this.sessionHost,
publish: (event) => this.publish(event),
buildEvent: buildHubEvent,
+2 -1
View File
@@ -44,7 +44,6 @@ export type {
ClineAccountActionRequest,
ConnectorHookEvent,
ContentBlock,
FeatureFlag,
FeatureFlagPayload,
FeatureFlagsAndPayloads,
FeatureFlagsContext,
@@ -100,6 +99,7 @@ export {
createTool,
emptyWorkspaceManifest,
FEATURE_FLAGS,
FeatureFlag,
FeatureFlagDefaultValue,
formatDisplayUserInput,
noopBasicLogger,
@@ -879,6 +879,7 @@ export {
type TelemetryServiceOptions,
} from "./services/telemetry/TelemetryService";
export {
createSessionCompactionSidecarEnabledResolver,
createSessionCompactionState,
parseSessionCompactionState,
projectSessionCompactionState,
@@ -109,6 +109,7 @@ function createLocalRuntimeHost(
toolPolicies: options.toolPolicies,
distinctId,
fetch: options.fetch,
isCompactionSidecarEnabled: options.isCompactionSidecarEnabled,
});
}
@@ -4413,6 +4413,86 @@ describe("LocalRuntimeHost", () => {
);
});
it("keeps compaction active but ignores sidecar state when the sidecar flag is off", async () => {
const sessionId = "sess-compaction-sidecar-off";
const manifest = createManifest(sessionId);
const initialMessages: MessageWithMetadata[] = [
{ role: "user", content: "canonical source" },
];
const initialCompactionState = createSessionCompactionState({
sourceMessages: initialMessages,
compactedMessages: [{ role: "user", content: "summary" }],
conversationId: sessionId,
updatedAt: "2026-01-01T00:00:00.000Z",
});
const sessionService = {
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
manifestPath: "/tmp/manifest-compaction-sidecar-off.json",
messagesPath: "/tmp/messages-compaction-sidecar-off.json",
manifest,
}),
persistSessionMessages: vi.fn(),
persistSessionCompactionState: vi.fn(),
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
writeSessionManifest: vi.fn(),
listSessions: vi.fn().mockResolvedValue([]),
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
};
const createAgent = vi.fn().mockReturnValue({
run: vi.fn().mockResolvedValue(createResult()),
continue: vi.fn(),
abort: vi.fn(),
subscribeEvents: vi.fn().mockReturnValue(() => {}),
canStartRun: vi.fn().mockReturnValue(true),
getAgentId: vi.fn().mockReturnValue("agent-root-1"),
getConversationId: vi.fn().mockReturnValue(sessionId),
restore: vi.fn(),
shutdown: vi.fn().mockResolvedValue(undefined),
getMessages: vi.fn().mockReturnValue(initialMessages),
messages: initialMessages,
});
const manager = new RuntimeHostUnderTest({
distinctId,
sessionService: sessionService as never,
runtimeBuilder: {
build: vi.fn().mockReturnValue({
tools: [],
shutdown: vi.fn(),
}),
},
createAgent: createAgent as never,
isCompactionSidecarEnabled: () => false,
});
await manager.startSession(
normalizeStartInput({
config: createConfig({
sessionId,
compaction: {
enabled: true,
strategy: "basic",
compact: vi.fn(),
},
}),
initialMessages,
initialCompactionState,
interactive: true,
}),
);
expect(createAgent.mock.calls[0]?.[0]?.prepareTurn).toEqual(
expect.any(Function),
);
expect(sessionService.persistSessionCompactionState).not.toHaveBeenCalled();
await expect(
manager.readSessionCompactionState(sessionId),
).resolves.toBeUndefined();
await expect(
manager.updateSessionCompactionState(sessionId, initialCompactionState),
).resolves.toEqual({ updated: false });
});
it("does not project compaction state when compaction is disabled", async () => {
const sessionId = "sess-compaction-disabled";
const manifest = createManifest(sessionId);
@@ -51,6 +51,7 @@ import {
} from "../../services/usage";
import { enrichPromptWithMentions } from "../../services/workspace";
import {
createSessionCompactionSidecarEnabledResolver,
projectSessionCompactionState,
type SessionCompactionState,
} from "../../session/models/session-compaction";
@@ -201,6 +202,7 @@ export interface LocalRuntimeHostOptions {
providerSettingsManager?: ProviderSettingsManager;
oauthTokenManager?: RuntimeOAuthTokenManager;
telemetry?: ITelemetryService;
isCompactionSidecarEnabled?: () => boolean;
/**
* Default custom `fetch` implementation threaded into every
* `ProviderConfig.fetch` built during local session bootstrap. Used by
@@ -222,6 +224,7 @@ export class LocalRuntimeHost implements RuntimeHost {
private readonly oauthTokenManager: RuntimeOAuthTokenManager;
private readonly defaultTelemetry?: ITelemetryService;
private readonly defaultFetch?: typeof fetch;
private readonly isCompactionSidecarEnabled: () => boolean;
private readonly events = new RuntimeHostEventBus();
private readonly sessions = new Map<string, ActiveSession>();
private readonly usageBySession = new Map<string, SessionAccumulatedUsage>();
@@ -254,10 +257,13 @@ export class LocalRuntimeHost implements RuntimeHost {
new RuntimeOAuthTokenManager({
providerSettingsManager: this.providerSettingsManager,
telemetry: options.telemetry,
});
});
this.defaultTelemetry = options.telemetry;
this.defaultTelemetry?.setDistinctId(distinctId);
this.defaultFetch = options.fetch;
this.isCompactionSidecarEnabled =
options.isCompactionSidecarEnabled ??
createSessionCompactionSidecarEnabledResolver();
this.pendingPromptsController = new PendingPromptsController({
getSession: (sid) => this.sessions.get(sid),
@@ -375,17 +381,18 @@ export class LocalRuntimeHost implements RuntimeHost {
);
if (existingManifest) {
manifest = existingManifest;
resumedArtifacts = {
manifestPath,
messagesPath: existingManifest.messages_path || messagesPath,
compactionPath: existingManifest.compaction_path,
manifest: existingManifest,
};
resumedCompactionState =
await this.invokeOptionalValue<SessionCompactionState>(
"readSessionCompactionState",
sessionId,
);
resumedArtifacts = {
manifestPath,
messagesPath: existingManifest.messages_path || messagesPath,
compactionPath: existingManifest.compaction_path,
manifest: existingManifest,
};
resumedCompactionState = this.isCompactionSidecarEnabled()
? await this.invokeOptionalValue<SessionCompactionState>(
"readSessionCompactionState",
sessionId,
)
: undefined;
}
}
const initialAggregateUsage = await this.seedAggregateUsageFromArtifacts({
@@ -485,10 +492,11 @@ export class LocalRuntimeHost implements RuntimeHost {
const explicitInitialCompactionState = startInput.initialCompactionState;
let activeSessionRef: ActiveSession | undefined;
const compact = createContextCompactionPrepareTurn(configWithProvider);
const sidecarEnabled = this.isCompactionSidecarEnabled();
const rawInitialCompactionState =
explicitInitialCompactionState ?? resumedCompactionState;
const initialCompactionState =
compact && rawInitialCompactionState
sidecarEnabled && compact && rawInitialCompactionState
? {
...rawInitialCompactionState,
conversation_id:
@@ -498,8 +506,10 @@ export class LocalRuntimeHost implements RuntimeHost {
const prepareTurn = compact
? createCompactionStateAwarePrepareTurn({
compact,
getState: () => activeSessionRef?.compactionState,
getState: () =>
sidecarEnabled ? activeSessionRef?.compactionState : undefined,
saveState: async (state) => {
if (!sidecarEnabled) return;
const activeSession = activeSessionRef;
if (!activeSession) return;
const stateForSession = {
@@ -1047,6 +1057,9 @@ export class LocalRuntimeHost implements RuntimeHost {
sessionId: string,
state: SessionCompactionState,
): Promise<{ updated: boolean }> {
if (!this.isCompactionSidecarEnabled()) {
return { updated: false };
}
const target = sessionId.trim();
if (!target) return { updated: false };
const activeSession = this.sessions.get(target);
@@ -1085,6 +1098,9 @@ export class LocalRuntimeHost implements RuntimeHost {
async readSessionCompactionState(
sessionId: string,
): Promise<SessionCompactionState | undefined> {
if (!this.isCompactionSidecarEnabled()) {
return undefined;
}
const target = sessionId.trim();
if (!target) return undefined;
const activeSession = this.sessions.get(target);
@@ -1,11 +1,31 @@
import { describe, expect, it } from "vitest";
import {
createSessionCompactionSidecarEnabledResolver,
createSessionCompactionState,
parseSessionCompactionState,
projectSessionCompactionState,
} from "./session-compaction";
describe("session compaction state", () => {
it("enables sidecar use unless the feature flag payload is explicitly false", () => {
expect(
createSessionCompactionSidecarEnabledResolver({
getFlagPayload: () => false,
})(),
).toBe(false);
expect(
createSessionCompactionSidecarEnabledResolver({
getFlagPayload: () => true,
})(),
).toBe(true);
expect(
createSessionCompactionSidecarEnabledResolver({
getFlagPayload: () => undefined,
})(),
).toBe(true);
expect(createSessionCompactionSidecarEnabledResolver()()).toBe(true);
});
it("rejects projection when the canonical prefix was edited before the boundary", () => {
const sourceMessages = [
{ id: "u1", role: "user" as const, content: "original detail" },
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import {
FeatureFlag,
formatDisplayUserInput,
type MessageWithMetadata,
} from "@cline/shared";
@@ -37,6 +38,17 @@ export type SessionCompactionState = z.infer<
typeof SessionCompactionStateSchema
>;
type CompactionSidecarFeatureFlags = {
getFlagPayload(flagName: string): unknown;
};
export function createSessionCompactionSidecarEnabledResolver(
featureFlags?: CompactionSidecarFeatureFlags,
): () => boolean {
return () =>
featureFlags?.getFlagPayload(FeatureFlag.COMPACTION_SIDECAR) !== false;
}
function cloneMessages(
messages: readonly MessageWithMetadata[],
): MessageWithMetadata[] {
+3
View File
@@ -1,6 +1,8 @@
export const FeatureFlag = {
/** Enables ClinePass provider/model list exposure in supported clients. */
CLINE_PASS: "ext-cline-pass",
/** Enables persisted compaction sidecar projection state. */
COMPACTION_SIDECAR: "sdk-compaction-sidecar",
} as const;
export type KnownFeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag];
@@ -62,6 +64,7 @@ export const FeatureFlagDefaultValue: Partial<
Record<FeatureFlag, FeatureFlagPayload | undefined>
> = {
[FeatureFlag.CLINE_PASS]: false,
[FeatureFlag.COMPACTION_SIDECAR]: true,
};
export const FEATURE_FLAGS: readonly FeatureFlag[] = Object.values(FeatureFlag);
+1 -1
View File
@@ -77,7 +77,7 @@ export {
export { PLUGIN_FILE_EXTENSIONS } from "./extensions/plugin";
export {
FEATURE_FLAGS,
type FeatureFlag,
FeatureFlag,
FeatureFlagDefaultValue,
type FeatureFlagPayload,
type FeatureFlagsAndPayloads,
+1 -1
View File
@@ -91,7 +91,7 @@ export {
export { PLUGIN_FILE_EXTENSIONS } from "./extensions/plugin";
export {
FEATURE_FLAGS,
type FeatureFlag,
FeatureFlag,
FeatureFlagDefaultValue,
type FeatureFlagPayload,
type FeatureFlagsAndPayloads,