Make session compaction adapter-aware
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { agents } from "@paperclipai/db";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
|
||||
import {
|
||||
parseSessionCompactionPolicy,
|
||||
resolveRuntimeSessionParamsForWorkspace,
|
||||
shouldResetTaskSessionForWake,
|
||||
type ResolvedWorkspaceForRun,
|
||||
@@ -20,6 +22,32 @@ function buildResolvedWorkspace(overrides: Partial<ResolvedWorkspaceForRun> = {}
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgent(adapterType: string, runtimeConfig: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
projectId: null,
|
||||
goalId: null,
|
||||
name: "Agent",
|
||||
role: "engineer",
|
||||
title: null,
|
||||
icon: null,
|
||||
status: "running",
|
||||
reportsTo: null,
|
||||
capabilities: null,
|
||||
adapterType,
|
||||
adapterConfig: {},
|
||||
runtimeConfig,
|
||||
budgetMonthlyCents: 0,
|
||||
spentMonthlyCents: 0,
|
||||
permissions: {},
|
||||
lastHeartbeatAt: null,
|
||||
metadata: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as unknown as typeof agents.$inferSelect;
|
||||
}
|
||||
|
||||
describe("resolveRuntimeSessionParamsForWorkspace", () => {
|
||||
it("migrates fallback workspace sessions to project workspace when project cwd becomes available", () => {
|
||||
const agentId = "agent-123";
|
||||
@@ -151,3 +179,55 @@ describe("shouldResetTaskSessionForWake", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSessionCompactionPolicy", () => {
|
||||
it("disables Paperclip-managed rotation by default for codex and claude local", () => {
|
||||
expect(parseSessionCompactionPolicy(buildAgent("codex_local"))).toEqual({
|
||||
enabled: true,
|
||||
maxSessionRuns: 0,
|
||||
maxRawInputTokens: 0,
|
||||
maxSessionAgeHours: 0,
|
||||
});
|
||||
expect(parseSessionCompactionPolicy(buildAgent("claude_local"))).toEqual({
|
||||
enabled: true,
|
||||
maxSessionRuns: 0,
|
||||
maxRawInputTokens: 0,
|
||||
maxSessionAgeHours: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps conservative defaults for adapters without confirmed native compaction", () => {
|
||||
expect(parseSessionCompactionPolicy(buildAgent("cursor"))).toEqual({
|
||||
enabled: true,
|
||||
maxSessionRuns: 200,
|
||||
maxRawInputTokens: 2_000_000,
|
||||
maxSessionAgeHours: 72,
|
||||
});
|
||||
expect(parseSessionCompactionPolicy(buildAgent("opencode_local"))).toEqual({
|
||||
enabled: true,
|
||||
maxSessionRuns: 200,
|
||||
maxRawInputTokens: 2_000_000,
|
||||
maxSessionAgeHours: 72,
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit agent overrides win over adapter defaults", () => {
|
||||
expect(
|
||||
parseSessionCompactionPolicy(
|
||||
buildAgent("codex_local", {
|
||||
heartbeat: {
|
||||
sessionCompaction: {
|
||||
maxSessionRuns: 25,
|
||||
maxRawInputTokens: 500_000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
enabled: true,
|
||||
maxSessionRuns: 25,
|
||||
maxRawInputTokens: 500_000,
|
||||
maxSessionAgeHours: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ServerAdapterModule } from "./types.js";
|
||||
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
execute as claudeExecute,
|
||||
testEnvironment as claudeTestEnvironment,
|
||||
@@ -70,6 +71,7 @@ const claudeLocalAdapter: ServerAdapterModule = {
|
||||
execute: claudeExecute,
|
||||
testEnvironment: claudeTestEnvironment,
|
||||
sessionCodec: claudeSessionCodec,
|
||||
sessionManagement: getAdapterSessionManagement("claude_local") ?? undefined,
|
||||
models: claudeModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
agentConfigurationDoc: claudeAgentConfigurationDoc,
|
||||
@@ -81,6 +83,7 @@ const codexLocalAdapter: ServerAdapterModule = {
|
||||
execute: codexExecute,
|
||||
testEnvironment: codexTestEnvironment,
|
||||
sessionCodec: codexSessionCodec,
|
||||
sessionManagement: getAdapterSessionManagement("codex_local") ?? undefined,
|
||||
models: codexModels,
|
||||
listModels: listCodexModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
@@ -93,6 +96,7 @@ const cursorLocalAdapter: ServerAdapterModule = {
|
||||
execute: cursorExecute,
|
||||
testEnvironment: cursorTestEnvironment,
|
||||
sessionCodec: cursorSessionCodec,
|
||||
sessionManagement: getAdapterSessionManagement("cursor") ?? undefined,
|
||||
models: cursorModels,
|
||||
listModels: listCursorModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
@@ -104,6 +108,7 @@ const geminiLocalAdapter: ServerAdapterModule = {
|
||||
execute: geminiExecute,
|
||||
testEnvironment: geminiTestEnvironment,
|
||||
sessionCodec: geminiSessionCodec,
|
||||
sessionManagement: getAdapterSessionManagement("gemini_local") ?? undefined,
|
||||
models: geminiModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
agentConfigurationDoc: geminiAgentConfigurationDoc,
|
||||
@@ -123,6 +128,7 @@ const openCodeLocalAdapter: ServerAdapterModule = {
|
||||
execute: openCodeExecute,
|
||||
testEnvironment: openCodeTestEnvironment,
|
||||
sessionCodec: openCodeSessionCodec,
|
||||
sessionManagement: getAdapterSessionManagement("opencode_local") ?? undefined,
|
||||
models: [],
|
||||
listModels: listOpenCodeModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
@@ -134,6 +140,7 @@ const piLocalAdapter: ServerAdapterModule = {
|
||||
execute: piExecute,
|
||||
testEnvironment: piTestEnvironment,
|
||||
sessionCodec: piSessionCodec,
|
||||
sessionManagement: getAdapterSessionManagement("pi_local") ?? undefined,
|
||||
models: [],
|
||||
listModels: listPiModels,
|
||||
supportsLocalAgentJwt: true,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// imports (process/, http/, heartbeat.ts) don't need rewriting.
|
||||
export type {
|
||||
AdapterAgent,
|
||||
AdapterSessionManagement,
|
||||
AdapterRuntime,
|
||||
UsageSummary,
|
||||
AdapterExecutionResult,
|
||||
@@ -15,5 +16,8 @@ export type {
|
||||
AdapterEnvironmentTestContext,
|
||||
AdapterSessionCodec,
|
||||
AdapterModel,
|
||||
NativeContextManagement,
|
||||
ResolvedSessionCompactionPolicy,
|
||||
SessionCompactionPolicy,
|
||||
ServerAdapterModule,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
|
||||
@@ -42,6 +42,11 @@ import {
|
||||
resolveExecutionWorkspaceMode,
|
||||
} from "./execution-workspace-policy.js";
|
||||
import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js";
|
||||
import {
|
||||
hasSessionCompactionThresholds,
|
||||
resolveSessionCompactionPolicy,
|
||||
type SessionCompactionPolicy,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
|
||||
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
||||
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
||||
@@ -49,14 +54,6 @@ const HEARTBEAT_MAX_CONCURRENT_RUNS_MAX = 10;
|
||||
const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
|
||||
const startLocksByAgent = new Map<string, Promise<void>>();
|
||||
const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
|
||||
const SESSIONED_LOCAL_ADAPTERS = new Set([
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"cursor",
|
||||
"gemini_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
]);
|
||||
|
||||
const heartbeatRunListColumns = {
|
||||
id: heartbeatRuns.id,
|
||||
@@ -133,13 +130,6 @@ type UsageTotals = {
|
||||
outputTokens: number;
|
||||
};
|
||||
|
||||
type SessionCompactionPolicy = {
|
||||
enabled: boolean;
|
||||
maxSessionRuns: number;
|
||||
maxRawInputTokens: number;
|
||||
maxSessionAgeHours: number;
|
||||
};
|
||||
|
||||
type SessionCompactionDecision = {
|
||||
rotate: boolean;
|
||||
reason: string | null;
|
||||
@@ -296,23 +286,8 @@ function formatCount(value: number | null | undefined) {
|
||||
return value.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
function parseSessionCompactionPolicy(agent: typeof agents.$inferSelect): SessionCompactionPolicy {
|
||||
const runtimeConfig = parseObject(agent.runtimeConfig);
|
||||
const heartbeat = parseObject(runtimeConfig.heartbeat);
|
||||
const compaction = parseObject(
|
||||
heartbeat.sessionCompaction ?? heartbeat.sessionRotation ?? runtimeConfig.sessionCompaction,
|
||||
);
|
||||
const supportsSessions = SESSIONED_LOCAL_ADAPTERS.has(agent.adapterType);
|
||||
const enabled = compaction.enabled === undefined
|
||||
? supportsSessions
|
||||
: asBoolean(compaction.enabled, supportsSessions);
|
||||
|
||||
return {
|
||||
enabled,
|
||||
maxSessionRuns: Math.max(0, Math.floor(asNumber(compaction.maxSessionRuns, 200))),
|
||||
maxRawInputTokens: Math.max(0, Math.floor(asNumber(compaction.maxRawInputTokens, 2_000_000))),
|
||||
maxSessionAgeHours: Math.max(0, Math.floor(asNumber(compaction.maxSessionAgeHours, 72))),
|
||||
};
|
||||
export function parseSessionCompactionPolicy(agent: typeof agents.$inferSelect): SessionCompactionPolicy {
|
||||
return resolveSessionCompactionPolicy(agent.adapterType, agent.runtimeConfig).policy;
|
||||
}
|
||||
|
||||
export function resolveRuntimeSessionParamsForWorkspace(input: {
|
||||
@@ -743,7 +718,7 @@ export function heartbeatService(db: Db) {
|
||||
}
|
||||
|
||||
const policy = parseSessionCompactionPolicy(agent);
|
||||
if (!policy.enabled) {
|
||||
if (!policy.enabled || !hasSessionCompactionThresholds(policy)) {
|
||||
return {
|
||||
rotate: false,
|
||||
reason: null,
|
||||
|
||||
Reference in New Issue
Block a user