Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/session/compactor-trigger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, expect, test } from "bun:test";
import { createPruningCompactor } from "./compactor.js";
import type {
ConversationTurn,
ReactorState,
StrategyContext,
} from "@intx/types/runtime";

function ctx(trigger: string): StrategyContext {
return { state: {} as ReactorState, trigger };
}

function textTurn(
role: ConversationTurn["role"],
text: string,
): ConversationTurn {
return {
role,
content: [{ type: "text", text }],
timestamp: 1,
};
}

// Bodies exceed the pinned tail budget so apply() takes the fold path.
function foldableTurns(): ConversationTurn[] {
const body = (label: string): string => `${label} ${"x".repeat(120)}`;
return [
textTurn("user", body("old message 1")),
textTurn("assistant", body("old response 1")),
textTurn("user", body("old message 2")),
textTurn("assistant", body("old response 2")),
textTurn("user", "recent message"),
textTurn("assistant", "recent response"),
];
}

function compactor(
summarize?: (turns: ConversationTurn[]) => Promise<string>,
): ReturnType<typeof createPruningCompactor> {
return createPruningCompactor({
summaryMaxChars: 2000,
compactionShape: { tailBudgetTokens: 10 },
...(summarize !== undefined ? { summarize } : {}),
});
}

describe("pruning compactor persists invocation trigger (CL-10869)", () => {
test.each(["director:operator-request", "director:context-threshold"])(
"a real fold records %s without rewriting the human reason",
async (trigger) => {
const result = await compactor().apply(foldableTurns(), ctx(trigger));
expect(result.record.reason).toMatch(
/^compacted \d+ turns, keeping \d+ tail$/,
);
expect(result.record.reason).not.toContain("director:");
expect(result.record.decisions.trigger).toBe(trigger);
},
);

test("a verify keep-prior record carries the trigger that was passed", async () => {
const trigger = "director:context-overflow";
const turns: ConversationTurn[] = [
textTurn(
"user",
"Migrate the auth module to opaque tokens in src/auth.ts",
),
textTurn("assistant", "Reading the handler first."),
{
role: "assistant",
content: [
{
type: "tool_call",
id: "c1",
name: "run_shell",
arguments: { command: "bun run test auth" },
},
],
timestamp: 3,
},
{
role: "user",
content: [
{
type: "tool_result",
callId: "c1",
isError: true,
content: [{ type: "text", text: "token refresh assertion failed" }],
},
],
timestamp: 4,
},
textTurn("assistant", "Fix the token refresh assertion next."),
textTurn("user", "recent ask"),
textTurn("assistant", "recent reply"),
];
const result = await compactor(
async () => "Auth migration done. No errors remain.",
).apply(turns, ctx(trigger));
expect(result.output).toBe(turns);
expect(result.record.reason).toBe("verify failed — keeping prior context");
expect(result.record.decisions.verifyAbortReason).toBe("contradiction");
expect(result.record.decisions.trigger).toBe(trigger);
});
});
33 changes: 21 additions & 12 deletions src/session/compactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,10 +903,19 @@ export function createPruningCompactor(
version: "1.9.0",
async apply(
turns: ConversationTurn[],
_ctx: StrategyContext,
ctx: StrategyContext,
): Promise<StrategyResult<ConversationTurn[]>> {
// Prior compacted summaries fold into the next handoff, not frozen.
const shape = resolveCompactionShape(cfg.compactionShape);
// Operator and automatic folds share one human reason string. Keep the
// reactor's raw trigger (director: prefix included) on the manifest so
// a later reader can tell them apart.
const withTrigger = (
fields: Record<string, unknown>,
): Record<string, unknown> => ({
...fields,
trigger: ctx.trigger,
});

if (turns.length <= COMPACT_NOOP_FLOOR) {
return {
Expand All @@ -919,7 +928,7 @@ export function createPruningCompactor(
...extraInstructionParameter(cfg),
},
reason: "no compaction needed",
decisions: { agedImageCount: 0 },
decisions: withTrigger({ agedImageCount: 0 }),
},
};
}
Expand Down Expand Up @@ -988,14 +997,14 @@ export function createPruningCompactor(
...extraInstructionParameter(cfg),
},
reason: "no compaction needed",
decisions: {
decisions: withTrigger({
tailBudgetTokens: shape.tailBudgetTokens,
tailTokenEstimate: tail.tailTokenEstimate,
shortenedToolOutputs: tail.shortenedToolOutputs,
agedImageCount: aged.agedImageCount,
supersededReadCount: supersededReads.size,
liveTokenEstimate: estimateContextTokens(output),
},
}),
},
...(aged.blobs.length > 0 ? { blobs: aged.blobs } : {}),
};
Expand Down Expand Up @@ -1036,10 +1045,10 @@ export function createPruningCompactor(
...extraInstructionParameter(cfg),
},
reason: "summarize failed",
decisions: {
decisions: withTrigger({
summarizeFailed: 1,
agedImageCount: aged.agedImageCount,
},
}),
},
};
}
Expand All @@ -1061,10 +1070,10 @@ export function createPruningCompactor(
...extraInstructionParameter(cfg),
},
reason: "summarize failed",
decisions: {
decisions: withTrigger({
summarizeFailed: 1,
agedImageCount: aged.agedImageCount,
},
}),
},
};
}
Expand All @@ -1090,12 +1099,12 @@ export function createPruningCompactor(
compactionShape: shape,
},
reason: "verify failed — keeping prior context",
decisions: {
decisions: withTrigger({
verifyAborted: 1,
verifyAbortReason: abortReason,
verifyMissing: misses.map((m) => m.kind),
agedImageCount: aged.agedImageCount,
},
}),
},
});
if (verified.aborted && verified.abortReason === "contradiction") {
Expand Down Expand Up @@ -1198,7 +1207,7 @@ export function createPruningCompactor(
? ` (statistics-only stub: ${summarizeFallback})`
: ""
}`,
decisions: {
decisions: withTrigger({
summarizedTurnCount: summarizedTurns.length,
anchorTurnCount: 0,
recentTurnCount: tailTurns.length,
Expand All @@ -1220,7 +1229,7 @@ export function createPruningCompactor(
}
: {}),
...verifyDecisions,
},
}),
},
blobs: [...aged.blobs, handoff.blob],
};
Expand Down
Loading