Skip to content

Commit 89d8d0f

Browse files
committed
fix(core): resolve dirty clusters before render, preserve tool output, async strategy flag
Bug fixes found via codex review: - Resolve dirty clusters before render() so merged cold messages aren't lost (P1: non-root members dropped when summary not yet cached) - Include 500-char tool response preview instead of name-only placeholder so error messages and file paths survive clustering (P1) - Make getCompressionStrategy() async with ensureExperimentsLoaded() so the union-find flag isn't ignored on first compression (P2) - Thread abortSignal into ClusterSummarizer; rethrow on abort instead of swallowing to fallback text (P2)
1 parent e74be4f commit 89d8d0f

3 files changed

Lines changed: 30 additions & 17 deletions

File tree

packages/core/src/config/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2350,9 +2350,8 @@ export class Config implements McpContext, AgentLoopContext {
23502350
if (this.experimentalJitContext && this.memoryContextManager) {
23512351
await this.memoryContextManager.refresh();
23522352
} else {
2353-
const { refreshServerHierarchicalMemory } = await import(
2354-
'../utils/memoryDiscovery.js'
2355-
);
2353+
const { refreshServerHierarchicalMemory } =
2354+
await import('../utils/memoryDiscovery.js');
23562355
await refreshServerHierarchicalMemory(this);
23572356
}
23582357
if (this._geminiClient?.isInitialized()) {
@@ -3025,7 +3024,8 @@ export class Config implements McpContext, AgentLoopContext {
30253024
return remoteThreshold;
30263025
}
30273026

3028-
getCompressionStrategy(): string {
3027+
async getCompressionStrategy(): Promise<string> {
3028+
await this.ensureExperimentsLoaded();
30293029
const remoteStrategy =
30303030
this.experiments?.flags[ExperimentFlags.COMPRESSION_STRATEGY]
30313031
?.stringValue;

packages/core/src/context/chatCompressionService.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export class ChatCompressionService {
263263
hasFailedCompressionAttempt: boolean,
264264
abortSignal?: AbortSignal,
265265
): Promise<{ newHistory: Content[] | null; info: ChatCompressionInfo }> {
266-
const strategy = config.getCompressionStrategy();
266+
const strategy = await config.getCompressionStrategy();
267267

268268
if (strategy === 'union-find') {
269269
return this.compactWithUnionFind(
@@ -537,7 +537,7 @@ export class ChatCompressionService {
537537
model: string,
538538
config: Config,
539539
hasFailedCompressionAttempt: boolean,
540-
_abortSignal?: AbortSignal,
540+
abortSignal?: AbortSignal,
541541
): Promise<{ newHistory: Content[] | null; info: ChatCompressionInfo }> {
542542
const curatedHistory = chat.getHistory(true);
543543

@@ -609,6 +609,7 @@ export class ChatCompressionService {
609609
const summarizer = new ClusterSummarizer(
610610
config.getBaseLlmClient(),
611611
modelStringToModelConfigAlias(model),
612+
abortSignal,
612613
);
613614
const contextWindow = new ContextWindow(embedder, summarizer, {
614615
graduateAt: UNION_FIND_GRADUATE_AT,
@@ -623,8 +624,16 @@ export class ChatCompressionService {
623624
?.map((p) => {
624625
if (p.text) return p.text;
625626
if (p.functionCall) return `[Tool call: ${p.functionCall.name}]`;
626-
if (p.functionResponse)
627-
return `[Tool response: ${p.functionResponse.name}]`;
627+
if (p.functionResponse) {
628+
const responseStr = JSON.stringify(
629+
p.functionResponse.response ?? '',
630+
);
631+
const preview =
632+
responseStr.length > 500
633+
? responseStr.slice(0, 500) + '...'
634+
: responseStr;
635+
return `[Tool response: ${p.functionResponse.name}] ${preview}`;
636+
}
628637
return '';
629638
})
630639
.join(' ')
@@ -634,18 +643,15 @@ export class ChatCompressionService {
634643
}
635644
}
636645

637-
// Render the compacted context (synchronous — uses cached summaries)
646+
// Resolve dirty clusters before rendering so summaries are available
647+
await contextWindow.resolveDirty();
648+
638649
const rendered = contextWindow.render(
639650
null,
640651
UNION_FIND_RETRIEVE_K,
641652
UNION_FIND_RETRIEVE_MIN_SIM,
642653
);
643654

644-
// Fire-and-forget: resolve dirty clusters in background
645-
// In production, this runs during the main LLM call wait.
646-
// Here we await it since there's no concurrent main call.
647-
await contextWindow.resolveDirty();
648-
649655
// Build new history: cold summaries as a single user message, then hot messages
650656
const coldSummaries = rendered.slice(
651657
0,

packages/core/src/services/clusterSummarizer.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ function sanitizePromptInput(value: string): string {
2626
export class ClusterSummarizer implements Summarizer {
2727
private _client: BaseLlmClient;
2828
private _modelConfigKey: string;
29+
private _abortSignal?: AbortSignal;
2930

30-
constructor(client: BaseLlmClient, modelConfigKey: string) {
31+
constructor(
32+
client: BaseLlmClient,
33+
modelConfigKey: string,
34+
abortSignal?: AbortSignal,
35+
) {
3136
this._client = client;
3237
this._modelConfigKey = modelConfigKey;
38+
this._abortSignal = abortSignal;
3339
}
3440

3541
async summarize(messages: string[]): Promise<string> {
@@ -50,13 +56,14 @@ export class ClusterSummarizer implements Summarizer {
5056
],
5157
promptId: 'cluster-summarize',
5258
role: LlmRole.UTILITY_COMPRESSOR,
53-
abortSignal: new AbortController().signal,
59+
abortSignal: this._abortSignal ?? new AbortController().signal,
5460
});
5561

5662
const text = getResponseText(response)?.trim();
5763
if (!text) return fallback;
5864
return text;
59-
} catch {
65+
} catch (e) {
66+
if (this._abortSignal?.aborted) throw e;
6067
return fallback;
6168
}
6269
}

0 commit comments

Comments
 (0)