refactor: compact copilot sessions through sdk state

Route Copilot compaction through SDK-backed state, remove marker sidecars, preserve auth/session binding behavior in SQLite-backed plugin state, and route Copilot CLI budget compaction through native harness compaction.
This commit is contained in:
Peter Steinberger
2026-06-01 01:18:46 -04:00
committed by GitHub
parent 4550cfa6a7
commit db4990d260
19 changed files with 1353 additions and 533 deletions
+18 -7
View File
@@ -280,6 +280,8 @@ describe("runCliTurnCompactionLifecycle", () => {
totalTokens: 950,
totalTokensFresh: true,
agentHarnessId: "codex",
authProfileOverride: "github-copilot:work",
authProfileOverrideSource: "auto",
};
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
@@ -368,9 +370,13 @@ describe("runCliTurnCompactionLifecycle", () => {
currentTokenCount: 950,
contextEngine,
agentHarnessId: "codex",
authProfileId: "github-copilot:work",
trigger: "budget",
force: true,
});
expect(compactAgentHarnessSessionCalls[0]?.[0].contextEngineRuntimeContext).toMatchObject({
authProfileId: "github-copilot:work",
});
expect(compactCalls).toHaveLength(0);
expect(recordCliCompactionInStore).toHaveBeenCalledTimes(1);
expect(recordCliCompactionInStore).toHaveBeenCalledWith(
@@ -383,11 +389,11 @@ describe("runCliTurnCompactionLifecycle", () => {
expect(updatedEntry?.compactionCount).toBe(1);
});
it("treats below-target Codex native CLI compaction as a no-op", async () => {
const sessionKey = "agent:main:codex-under-target";
const sessionId = "session-codex-under-target";
const sessionFile = path.join(tmpDir, "session-codex-under-target.jsonl");
const storePath = path.join(tmpDir, "sessions-codex-under-target.json");
it("treats below-target Copilot native CLI compaction as a no-op", async () => {
const sessionKey = "agent:main:copilot-under-target";
const sessionId = "session-copilot-under-target";
const sessionFile = path.join(tmpDir, "session-copilot-under-target.jsonl");
const storePath = path.join(tmpDir, "sessions-copilot-under-target.json");
await writeSessionFile({ sessionFile, sessionId });
const sessionEntry: SessionEntry = {
@@ -397,7 +403,7 @@ describe("runCliTurnCompactionLifecycle", () => {
contextTokens: 1_000,
totalTokens: 950,
totalTokensFresh: true,
agentHarnessId: "codex",
agentHarnessId: "copilot",
};
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
@@ -441,7 +447,7 @@ describe("runCliTurnCompactionLifecycle", () => {
sessionAgentId: "main",
workspaceDir: tmpDir,
agentDir: tmpDir,
provider: "codex",
provider: "github-copilot",
model: "gpt-5.5",
});
@@ -934,6 +940,8 @@ describe("runCliTurnCompactionLifecycle", () => {
totalTokens: 950,
totalTokensFresh: true,
agentHarnessId: "codex",
authProfileOverride: "github-copilot:work",
authProfileOverrideSource: "auto",
};
const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
@@ -992,6 +1000,9 @@ describe("runCliTurnCompactionLifecycle", () => {
expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1);
expect(compactCalls).toHaveLength(1);
expect(compactCalls[0]?.runtimeContext).toMatchObject({
authProfileId: "github-copilot:work",
});
expect(maintenance).toHaveBeenCalledTimes(1);
expect(recordCliCompactionInStore).toHaveBeenCalledWith(
expect.objectContaining({
+11 -3
View File
@@ -87,6 +87,7 @@ type CliCompactionRuntimeContextParams = {
sessionKey: string;
messageChannel?: string;
agentAccountId?: string;
authProfileId?: string;
workspaceDir: string;
cwd?: string;
agentDir: string;
@@ -174,8 +175,8 @@ function isNativeHarnessCompactionSession(
const providerId = provider.trim().toLowerCase();
return (
harnessId === providerId ||
(harnessId === "codex" &&
(providerId === "codex" || providerId === "openai" || providerId === "openai"))
(harnessId === "copilot" && providerId === "github-copilot") ||
(harnessId === "codex" && (providerId === "codex" || providerId === "openai"))
);
}
@@ -211,7 +212,7 @@ function buildCliCompactionRuntimeContext(params: CliCompactionRuntimeContextPar
messageChannel: params.messageChannel,
messageProvider: params.messageChannel,
agentAccountId: params.agentAccountId,
authProfileId: undefined,
authProfileId: params.authProfileId,
workspaceDir: params.workspaceDir,
cwd: params.cwd,
agentDir: params.agentDir,
@@ -246,6 +247,7 @@ async function compactCliTranscript(params: {
skillsSnapshot?: SkillSnapshot;
messageChannel?: string;
agentAccountId?: string;
authProfileId?: string;
senderIsOwner?: boolean;
thinkLevel?: Parameters<typeof buildEmbeddedCompactionRuntimeContext>[0]["thinkLevel"];
extraSystemPrompt?: string;
@@ -255,6 +257,7 @@ async function compactCliTranscript(params: {
sessionKey: params.sessionKey,
messageChannel: params.messageChannel,
agentAccountId: params.agentAccountId,
authProfileId: params.authProfileId,
workspaceDir: params.workspaceDir,
cwd: params.cwd,
agentDir: params.agentDir,
@@ -360,6 +363,7 @@ async function compactNativeHarnessCliTranscript(params: {
try {
const sessionAgentId = readAgentIdFromSessionKey(params.sessionKey);
const nativeHarnessId = params.sessionEntry.agentHarnessId?.trim();
const authProfileId = params.sessionEntry.authProfileOverride?.trim() || undefined;
await cliCompactionDeps.ensureSelectedAgentHarnessPlugin({
provider: params.provider,
modelId: params.model,
@@ -382,6 +386,7 @@ async function compactNativeHarnessCliTranscript(params: {
skillsSnapshot: params.skillsSnapshot,
provider: params.provider,
model: params.model,
authProfileId,
contextTokenBudget: params.contextTokenBudget,
currentTokenCount: params.currentTokenCount,
trigger: "budget",
@@ -399,6 +404,7 @@ async function compactNativeHarnessCliTranscript(params: {
sessionKey: params.sessionKey,
messageChannel: params.messageChannel,
agentAccountId: params.agentAccountId,
authProfileId,
workspaceDir: params.workspaceDir,
cwd: params.cwd,
agentDir: params.agentDir,
@@ -526,6 +532,7 @@ export async function runCliTurnCompactionLifecycle(params: {
let nativeFallbackNeedsBindingClear = false;
let resolvedContextEngine: ContextEngine | undefined;
let autoCompactionGuardApplied = false;
const authProfileId = params.sessionEntry?.authProfileOverride?.trim() || undefined;
const applyAutoCompactionGuard = async (contextEngine: ContextEngine): Promise<void> => {
if (autoCompactionGuardApplied) {
return;
@@ -606,6 +613,7 @@ export async function runCliTurnCompactionLifecycle(params: {
skillsSnapshot: params.skillsSnapshot,
messageChannel: params.messageChannel,
agentAccountId: params.agentAccountId,
authProfileId,
senderIsOwner: params.senderIsOwner,
thinkLevel: params.thinkLevel,
extraSystemPrompt: params.extraSystemPrompt,
@@ -27,6 +27,8 @@ export type CompactEmbeddedAgentSessionParams = {
senderUsername?: string;
senderE164?: string;
authProfileId?: string;
/** Host-resolved provider credential for native harness compaction. */
resolvedApiKey?: string;
/** Group id for channel-level tool policy resolution. */
groupId?: string | null;
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
+91 -4
View File
@@ -21,6 +21,10 @@ import type { AgentHarness } from "./types.js";
const agentRunAttempt = vi.fn<AgentHarness["runAttempt"]>(async () =>
createAttemptResult("openclaw"),
);
const compactAuthMocks = vi.hoisted(() => ({
getApiKeyForModel: vi.fn(),
resolveModelAsync: vi.fn(),
}));
vi.mock("./builtin-openclaw.js", () => ({
createOpenClawAgentHarness: (): AgentHarness => ({
@@ -31,11 +35,21 @@ vi.mock("./builtin-openclaw.js", () => ({
runAttempt: agentRunAttempt,
}),
}));
vi.mock("../model-auth.js", () => ({
getApiKeyForModel: compactAuthMocks.getApiKeyForModel,
}));
vi.mock("../embedded-agent-runner/model.js", () => ({
resolveModelAsync: compactAuthMocks.resolveModelAsync,
}));
const originalRuntime = process.env.OPENCLAW_AGENT_RUNTIME;
beforeEach(() => {
clearAgentHarnesses();
compactAuthMocks.resolveModelAsync.mockResolvedValue({
model: { id: "gpt-5.5", provider: "openai" },
});
compactAuthMocks.getApiKeyForModel.mockResolvedValue({ apiKey: "test-key" });
cliBackendsTesting.setDepsForTest({
resolvePluginSetupRegistry: () => ({
providers: [],
@@ -65,6 +79,8 @@ afterEach(() => {
clearAgentHarnesses();
cliBackendsTesting.resetDepsForTest();
agentRunAttempt.mockClear();
compactAuthMocks.resolveModelAsync.mockReset();
compactAuthMocks.getApiKeyForModel.mockReset();
if (originalRuntime == null) {
delete process.env.OPENCLAW_AGENT_RUNTIME;
} else {
@@ -757,7 +773,10 @@ describe("selectAgentHarness", () => {
});
it("honors selected plugin harness pins during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
const compact = vi.fn<NonNullable<AgentHarness["compact"]>>(async () => ({
ok: true,
compacted: false,
}));
registerAgentHarness(
{
id: "codex",
@@ -779,10 +798,68 @@ describe("selectAgentHarness", () => {
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
agents: {
list: [{ id: "main", default: true, agentDir: "/tmp/main-agent" }],
defaults: {
models: {
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
},
},
},
} as OpenClawConfig,
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
expect(compact.mock.calls[0]?.[0]).toMatchObject({
agentDir: "/tmp/main-agent",
agentId: "main",
});
});
it("keeps compaction recoverable when auth profile lookup fails", async () => {
compactAuthMocks.getApiKeyForModel.mockRejectedValue(new Error("missing auth profile"));
const compact = vi.fn<NonNullable<AgentHarness["compact"]>>(async () => ({
ok: true,
compacted: false,
}));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
authProfileId: "deleted-profile",
agentHarnessId: "codex",
config: agentModelRuntimeConfig("openai/gpt-5.5", "openclaw"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
expect(compact.mock.calls[0]?.[0]).not.toHaveProperty("resolvedApiKey");
expect(compactAuthMocks.resolveModelAsync).toHaveBeenCalledWith(
"openai",
"gpt-5.5",
expect.any(String),
expect.any(Object),
expect.objectContaining({
authProfileId: "deleted-profile",
workspaceDir: "/tmp/workspace",
}),
);
});
it("does not compact a selected plugin harness through OpenClaw when the plugin has no compactor", async () => {
@@ -807,7 +884,10 @@ describe("selectAgentHarness", () => {
});
it("uses agent-scoped runtime policy during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
const compact = vi.fn<NonNullable<AgentHarness["compact"]>>(async () => ({
ok: true,
compacted: false,
}));
registerAgentHarness(
{
id: "codex",
@@ -836,7 +916,10 @@ describe("selectAgentHarness", () => {
});
it("uses sandbox session key for compaction preflight runtime policy", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
const compact = vi.fn<NonNullable<AgentHarness["compact"]>>(async () => ({
ok: true,
compacted: false,
}));
registerAgentHarness(
{
id: "codex",
@@ -863,10 +946,14 @@ describe("selectAgentHarness", () => {
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
expect(compact.mock.calls[0]?.[0]).toMatchObject({ agentId: "main" });
});
it("keeps explicit agent id for non-agent sandbox policy keys during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
const compact = vi.fn<NonNullable<AgentHarness["compact"]>>(async () => ({
ok: true,
compacted: false,
}));
registerAgentHarness(
{
id: "codex",
+77 -1
View File
@@ -2,7 +2,9 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolveUserPath } from "../../utils.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import {
resolveEffectiveToolPolicy,
resolveGroupToolPolicy,
@@ -10,11 +12,13 @@ import {
resolveSubagentToolPolicyForSession,
} from "../agent-tools.policy.js";
import type { CompactEmbeddedAgentSessionParams } from "../embedded-agent-runner/compact.types.js";
import { resolveModelAsync } from "../embedded-agent-runner/model.js";
import type {
EmbeddedRunAttemptParams,
EmbeddedRunAttemptResult,
} from "../embedded-agent-runner/run/types.js";
import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js";
import { getApiKeyForModel } from "../model-auth.js";
import { isCliRuntimeAliasForProvider, isCliRuntimeProvider } from "../model-runtime-aliases.js";
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { resolveSenderToolPolicy } from "../sender-tool-policy.js";
@@ -485,6 +489,61 @@ function logAgentHarnessSelection(
});
}
function resolveHarnessCompactIdentity(params: CompactEmbeddedAgentSessionParams): {
agentDir: string;
agentId: string;
} {
const agentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
return {
agentDir: params.agentDir ?? resolveAgentDir(params.config ?? {}, agentIds.sessionAgentId),
agentId: params.agentId ?? agentIds.sessionAgentId,
};
}
async function resolveHarnessCompactApiKey(params: {
agentDir: string;
compactParams: CompactEmbeddedAgentSessionParams;
}): Promise<string | undefined> {
const { agentDir, compactParams } = params;
const existing = compactParams.resolvedApiKey?.trim();
if (existing) {
return existing;
}
if (
!compactParams.authProfileId?.trim() ||
!compactParams.provider?.trim() ||
!compactParams.model?.trim()
) {
return undefined;
}
const workspaceDir = resolveUserPath(compactParams.workspaceDir);
const { model } = await resolveModelAsync(
compactParams.provider,
compactParams.model,
agentDir,
compactParams.config,
{
authProfileId: compactParams.authProfileId,
workspaceDir,
},
);
if (!model) {
return undefined;
}
const apiKeyInfo = await getApiKeyForModel({
model,
cfg: compactParams.config,
profileId: compactParams.authProfileId,
agentDir,
workspaceDir,
});
return apiKeyInfo.apiKey?.trim() || undefined;
}
export async function maybeCompactAgentHarnessSession(
params: CompactEmbeddedAgentSessionParams,
): Promise<EmbeddedAgentCompactResult | undefined> {
@@ -539,7 +598,24 @@ export async function maybeCompactAgentHarnessSession(
}
return undefined;
}
return harness.compact(params);
const compactIdentity = resolveHarnessCompactIdentity(params);
const compactParams = {
...params,
agentDir: compactIdentity.agentDir,
agentId: compactIdentity.agentId,
};
let resolvedApiKey: string | undefined;
try {
resolvedApiKey = await resolveHarnessCompactApiKey({
agentDir: compactIdentity.agentDir,
compactParams,
});
} catch (err) {
log.debug("agent harness compaction credential lookup failed", {
error: formatErrorMessage(err),
});
}
return harness.compact(resolvedApiKey ? { ...compactParams, resolvedApiKey } : compactParams);
}
function formatProviderModel(params: { provider: string; modelId?: string }): string {
return params.modelId ? `${params.provider}/${params.modelId}` : params.provider;
@@ -165,6 +165,7 @@ describe("handleCompactCommand", () => {
space: "workspace-1",
spawnedBy: "agent:main:parent",
totalTokens: 12345,
authProfileOverride: "github-copilot:work",
},
} as HandleCommandsParams,
true,
@@ -188,6 +189,7 @@ describe("handleCompactCommand", () => {
expect(call.senderUsername).toBe("alice_u");
expect(call.senderE164).toBe("+15551234567");
expect(call.agentDir).toBe("/tmp/openclaw-agent-compact");
expect(call.authProfileId).toBe("github-copilot:work");
});
it("treats already-under-target manual compaction as skipped", async () => {
+1
View File
@@ -273,6 +273,7 @@ export const handleCompactCommand: CommandHandler = async (params) => {
skillsSnapshot: targetSessionEntry.skillsSnapshot,
provider: params.provider,
model: params.model,
authProfileId: targetSessionEntry.authProfileOverride,
contextTokenBudget,
agentHarnessId:
targetSessionEntry.sessionId === sessionId ? targetSessionEntry.agentHarnessId : undefined,
+1
View File
@@ -2648,6 +2648,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
config: cfg,
provider: resolvedModel.provider,
model: resolvedModel.model,
authProfileId: entry?.authProfileOverride,
agentHarnessId: entry?.sessionId === sessionId ? entry.agentHarnessId : undefined,
thinkLevel: normalizeThinkLevel(entry?.thinkingLevel),
reasoningLevel: normalizeReasoningLevel(entry?.reasoningLevel),
@@ -710,7 +710,12 @@ test("sessions.compact scopes selected global truncation to the requested agent"
await fs.writeFile(
workStorePath,
JSON.stringify(
{ global: sessionStoreEntry("sess-work-global", { sessionFile: workTranscript }) },
{
global: sessionStoreEntry("sess-work-global", {
authProfileOverride: "github-copilot:work",
sessionFile: workTranscript,
}),
},
null,
2,
),
@@ -799,7 +804,12 @@ test("sessions.compact passes the selected global agent into embedded compaction
await fs.writeFile(
workStorePath,
JSON.stringify(
{ global: sessionStoreEntry("sess-work-global", { sessionFile: workTranscript }) },
{
global: sessionStoreEntry("sess-work-global", {
authProfileOverride: "github-copilot:work",
sessionFile: workTranscript,
}),
},
null,
2,
),
@@ -851,6 +861,7 @@ test("sessions.compact passes the selected global agent into embedded compaction
sessionId: "sess-work-global",
sessionKey: "global",
agentId: "work",
authProfileId: "github-copilot:work",
});
testState.sessionStorePath = undefined;
testState.sessionConfig = undefined;
+1
View File
@@ -286,6 +286,7 @@ export {
// timeout the built-in embedded-agent runner uses — one shared implementation, no
// copy-pasted watchdog.
export {
compactWithSafetyTimeout,
compactContextEngineWithSafetyTimeout,
resolveCompactionTimeoutMs,
} from "../agents/embedded-agent-runner/compaction-safety-timeout.js";