Skip to content

Commit 08d0035

Browse files
committed
Pass workflow context through the session compaction path
The TUI wrapped summarize to inject the active workflow, but the pruning compactor always called summarize with one argument, so mid-workflow compaction still lost the step contract. Also make the max-anchor fixture score above the new threshold so the cap test actually exercises it.
1 parent adf2ea4 commit 08d0035

4 files changed

Lines changed: 60 additions & 24 deletions

File tree

‎src/context-compactor.test.ts‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -485,18 +485,26 @@ describe("createPruningCompactor — maxAnchorTurns caps pairing pulls (CL-6906)
485485
const turns: ConversationTurn[] = [
486486
makeTurn({ role: "user", content: [{ type: "text", text: "the initiating task" }] }),
487487
];
488-
// 10 edit_file call/result pairs, well separated from each other and from
489-
// the recent window, each independently clearing the anchor threshold.
488+
// 10 write-pair call/result turns, well separated from each other and from
489+
// the recent window. A single edit_file scores 3 (below the threshold of
490+
// 5); two writes on the same assistant turn score 6, so each pair
491+
// independently clears the scored-anchor bar.
490492
for (let i = 0; i < 10; i++) {
491493
turns.push(
492494
makeTurn({
493495
role: "assistant",
494496
content: [
495497
{
496498
type: "tool_call",
497-
id: `edit${i}`,
499+
id: `edit${i}a`,
498500
name: "edit_file",
499-
arguments: { path: `f${i}.ts` },
501+
arguments: { path: `f${i}a.ts` },
502+
},
503+
{
504+
type: "tool_call",
505+
id: `edit${i}b`,
506+
name: "edit_file",
507+
arguments: { path: `f${i}b.ts` },
500508
},
501509
],
502510
}),
@@ -505,8 +513,13 @@ describe("createPruningCompactor — maxAnchorTurns caps pairing pulls (CL-6906)
505513
content: [
506514
{
507515
type: "tool_result",
508-
callId: `edit${i}`,
509-
content: [{ type: "text", text: `edited f${i}.ts` }],
516+
callId: `edit${i}a`,
517+
content: [{ type: "text", text: `edited f${i}a.ts` }],
518+
},
519+
{
520+
type: "tool_result",
521+
callId: `edit${i}b`,
522+
content: [{ type: "text", text: `edited f${i}b.ts` }],
510523
},
511524
],
512525
}),

‎src/session/runtime-assembly.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,26 @@ describe("createSessionPruningCompactor", () => {
267267
expect(typeof pruning.apply).toBe("function");
268268
expect(typeof llm.apply).toBe("function");
269269
});
270+
271+
test("forwards summaryContext to summarize in llm mode", async () => {
272+
const ctx = { workflow: { name: "build", stepIndex: 1, total: 3 } };
273+
let captured: unknown;
274+
const summarize = async (_turns: unknown, c?: unknown) => {
275+
captured = c;
276+
return "summary";
277+
};
278+
const llm = createSessionPruningCompactor({
279+
compactionMode: "llm",
280+
summarize,
281+
summaryContext: () => ctx,
282+
});
283+
const now = Date.now();
284+
const turns = Array.from({ length: 8 }, (_, i) => ({
285+
role: i % 2 === 0 ? "user" : "assistant",
286+
content: [{ type: "text", text: `t${i}` }],
287+
timestamp: now,
288+
}));
289+
await llm.apply(turns as never, { state: {} as never, trigger: "test" });
290+
expect(captured).toBe(ctx);
291+
});
270292
});

‎src/session/runtime-assembly.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type { Approval, GrantScope } from "../permission/types.js";
4141
import type { ReasoningEffort } from "../provider/reasoning-effort.js";
4242
import type { SubAgentProvider } from "../subagent/index.js";
4343
import { COMPACTOR_KEEP_RECENT_TURNS, createPruningCompactor } from "./compactor.js";
44+
import type { SummaryContext } from "./summarizer.js";
4445
import { NOOP_TELEMETRY, type Telemetry } from "../telemetry/index.js";
4546

4647
// ---------------------------------------------------------------------------
@@ -274,7 +275,8 @@ const SESSION_COMPACTOR_SUMMARY_MAX_CHARS = 2500;
274275

275276
export type SessionPruningCompactorArgs = {
276277
compactionMode: "llm" | "pruning";
277-
summarize: (turns: ConversationTurn[]) => Promise<string>;
278+
summarize: (turns: ConversationTurn[], ctx?: SummaryContext) => Promise<string>;
279+
summaryContext?: () => SummaryContext | undefined;
278280
telemetry?: Telemetry;
279281
};
280282

@@ -286,6 +288,7 @@ export function createSessionPruningCompactor(
286288
keepRecentTurns: COMPACTOR_KEEP_RECENT_TURNS,
287289
summaryMaxChars: SESSION_COMPACTOR_SUMMARY_MAX_CHARS,
288290
...(args.compactionMode !== "pruning" ? { summarize: args.summarize } : {}),
291+
...(args.summaryContext ? { summaryContext: args.summaryContext } : {}),
289292
});
290293
const telemetry = args.telemetry ?? NOOP_TELEMETRY;
291294
return {

‎src/tui/runner.ts‎

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ import {
221221
skillDirsFromEnabledPlugins,
222222
} from "../session/runtime-assembly.js";
223223
import { createAttachmentRehydrateTransform } from "../session/attachment-store.js";
224-
import { createModelSummarizer } from "../session/summarizer.js";
224+
import { createModelSummarizer, type SummaryContext } from "../session/summarizer.js";
225225
import { COMMAND_NAME, ID_PREFIX, LOG_NAMESPACE_ROOT } from "../branding.js";
226226
import { deliverAgentMessage } from "./deliver-agent-message.js";
227227

@@ -1394,23 +1394,20 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13941394

13951395
// Compaction summarizer: produces a structured, workflow-aware handoff via a
13961396
// one-shot call on the live model, falling back to the deterministic summary
1397-
// on any failure. The workflow context is read at call time so a compaction
1398-
// mid-/build or mid-/plan preserves which step we are on.
1397+
// on any failure. Workflow state is read at compaction time so a pass
1398+
// mid-/build or mid-/plan still names the active step.
13991399
const compactionSummarize = createModelSummarizer({ getSource: () => liveSource, deps: inferenceDeps });
1400-
const summarizeForCompaction = (turns: Parameters<typeof compactionSummarize>[0]): Promise<string> => {
1400+
const summaryContext = (): SummaryContext | undefined => {
14011401
const status = workflowController.status();
1402-
return compactionSummarize(turns, {
1403-
...(status.active
1404-
? {
1405-
workflow: {
1406-
...(status.name !== undefined ? { name: status.name } : {}),
1407-
stepLabel: status.label,
1408-
stepIndex: status.stepIndex,
1409-
total: status.total,
1410-
},
1411-
}
1412-
: {}),
1413-
});
1402+
if (!status.active) return undefined;
1403+
return {
1404+
workflow: {
1405+
...(status.name !== undefined ? { name: status.name } : {}),
1406+
stepLabel: status.label,
1407+
stepIndex: status.stepIndex,
1408+
total: status.total,
1409+
},
1410+
};
14141411
};
14151412

14161413
// Mutable reference so the compaction summarize callback reads the live mode
@@ -1441,7 +1438,8 @@ export async function runTUI(initialConfig: Config): Promise<number> {
14411438
compactors: {
14421439
"pruning-compactor": createSessionPruningCompactor({
14431440
compactionMode: liveCompactionMode,
1444-
summarize: summarizeForCompaction,
1441+
summarize: compactionSummarize,
1442+
summaryContext,
14451443
telemetry: liveTelemetry,
14461444
}),
14471445
},

0 commit comments

Comments
 (0)