Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
74ff07f
feat(agent-core): rework compaction to keep only user prompts and sum…
7Sageer Jun 25, 2026
c069a3c
Merge remote-tracking branch 'upstream/main' into codex-compaction
7Sageer Jun 29, 2026
32c70b3
fix(agent-core): compaction follow-ups
7Sageer Jun 29, 2026
53b92ec
feat(agent-core): refresh system prompt after compaction
7Sageer Jun 29, 2026
638c0f7
fix(agent-core): re-inject plan reminder after compaction
7Sageer Jun 29, 2026
82ae5ee
fix(agent-core): bound overflow compaction retries
7Sageer Jun 29, 2026
5a3ddf5
fix(agent-core): reinject auto permission reminder after compaction
7Sageer Jun 29, 2026
ececfb1
fix(agent-core): compaction rework follow-ups
7Sageer Jun 29, 2026
8196885
fix(agent-core): preserve compaction bookkeeping across resume
7Sageer Jun 29, 2026
25bc2a6
Merge remote-tracking branch 'upstream/main' into codex-compaction
7Sageer Jun 29, 2026
c14d30d
refactor(agent-core): dedupe real-user-input check and tighten mement…
7Sageer Jun 29, 2026
15c4e05
refactor(agent-core): centralize compaction retention policy
7Sageer Jun 29, 2026
800792a
fix(agent-core): tighten compaction context refresh
7Sageer Jun 29, 2026
5a0b3ca
fix(kosong): merge only same-kind consecutive user messages in anthro…
7Sageer Jun 29, 2026
be6ea51
fix(agent-core): restore compaction instruction structure
7Sageer Jun 29, 2026
d58625a
fix(agent-core): render compaction custom instruction via template
7Sageer Jun 29, 2026
6005045
fix(agent-core): harden compaction edge cases
7Sageer Jun 29, 2026
4baee98
fix(agent-core): avoid leaking compaction summary prefix
7Sageer Jun 29, 2026
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
5 changes: 5 additions & 0 deletions .changeset/compaction-internals-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Tighten compaction bookkeeping so compacted history stays consistent across retries and resume.
5 changes: 5 additions & 0 deletions .changeset/rework-compaction-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Rework conversation compaction to keep only real user prompts followed by a user-role summary, dropping assistant and tool messages.
93 changes: 53 additions & 40 deletions apps/vis/server/src/lib/context-projector.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
COMPACT_USER_MESSAGE_MAX_TOKENS,
collectCompactableUserMessages,
isRealUserInput,
selectRecentUserMessages,
} from '@moonshot-ai/agent-core';
import type {
ContentPart,
ContextMessage,
Expand Down Expand Up @@ -234,19 +240,21 @@ export function projectContext(
break;
case 'context.apply_compaction': {
openSteps = new Map();
// Mirror agent-core's actual `applyCompaction` behaviour
// (`packages/agent-core/src/agent/context/index.ts`): history becomes
// `[summaryBubble, ...history.slice(compactedCount)]`. The summary is
// an *assistant* message tagged `origin.kind = 'compaction_summary'`
// (using 'system' would skew role counts and any downstream diff
// against agent-core history). The post-compaction tail is preserved
// rather than dropped, so messages still in context stay visible.
// Mirror agent-core's `applyCompaction`
// (`packages/agent-core/src/agent/context/index.ts`): the live history
// becomes the most recent real user messages (verbatim, within a token
// budget) followed by a single user-role summary tagged
// `origin.kind = 'compaction_summary'`. Assistant messages, tool calls,
// and tool results are dropped. The selection rule
// (`selectRecentUserMessages` / `collectCompactableUserMessages`) is the
// same helper agent-core's `ContextMemory` and the web transcript
// reducer apply, so all three views stay in sync.
const summaryBubble: ProjectedMessage = {
lineNo: entry.lineNo,
time: rec.time,
source: 'compaction_summary',
message: {
role: 'assistant',
role: 'user',
content: [{ type: 'text', text: rec.summary }],
toolCalls: [],
origin: { kind: 'compaction_summary' },
Expand All @@ -258,34 +266,49 @@ export function projectContext(
tokensAfter: rec.tokensAfter,
},
};
const modelSummaryBubble: ProjectedMessage =
rec.contextSummary === undefined
? summaryBubble
: {
...summaryBubble,
message: {
...summaryBubble.message,
content: [{ type: 'text', text: rec.contextSummary }],
} as ContextMessage,
};
if (mode === 'model') {
// Drop the first `rec.compactedCount` HISTORY entries (NOT array
// entries): agent-core's `compactedCount` indexes into `_history`,
// which never contains our synthetic 'undo'/'clear' markers. Walk the
// array counting only history entries (`isHistoryEntry`) until
// `compactedCount` are passed, then slice there — any UI-only markers
// in the dropped region go with it (correct: they precede the
// compaction). With no markers this is exactly `slice(compactedCount)`.
let sliceAt = messages.length;
let passed = 0;
for (let i = 0; i < messages.length; i++) {
if (passed >= rec.compactedCount) {
sliceAt = i;
break;
}
if (isHistoryEntry(messages[i]!)) passed++;
}
if (passed < rec.compactedCount) sliceAt = messages.length;
messages = [summaryBubble, ...messages.slice(sliceAt)];
// Rebuild the model's-eye view as the kept user messages + summary.
// `realUserEntries` is filtered with the exact
// `collectCompactableUserMessages` predicate so it stays aligned with
// the selection below (genuine user input only — no injections,
// system triggers, or prior summaries). `selectRecentUserMessages`
// keeps a contiguous suffix of that subsequence, with only the oldest
// kept message possibly truncated, so each kept message maps back onto
// its original ProjectedMessage wrapper (preserving line/time); we swap
// in the (possibly truncated) message object.
const historyEntries = messages.filter(isHistoryEntry);
const realUserEntries = historyEntries.filter(
(pm) => collectCompactableUserMessages([pm.message]).length === 1,
);
const keptUserMessages = selectRecentUserMessages(
realUserEntries.map((pm) => pm.message),
COMPACT_USER_MESSAGE_MAX_TOKENS,
);
const suffixStart = realUserEntries.length - keptUserMessages.length;
const keptEntries: ProjectedMessage[] = keptUserMessages.map((message, i) => {
const original = realUserEntries[suffixStart + i]!;
return original.message === message ? original : { ...original, message };
});
messages = [...keptEntries, modelSummaryBubble];
} else {
// Full history: keep ALL preceding messages, just append the summary
// marker inline so the compacted prefix stays visible.
messages.push(summaryBubble);
}
// Mirror agent-core applyCompaction() → microCompaction.reset() (cutoff
// → 0): the message list is rebuilt as [summary, ...tail], so the old
// index-based cutoff no longer points at the same messages. (In full
// mode the blanking pass does not run, so this is a no-op there.)
// → 0): the message list is rebuilt, so the old index-based cutoff no
// longer points at the same messages. (In full mode the blanking pass
// does not run, so this is a no-op there.)
microCutoff = 0;
// Mirror agent-core applyCompaction() → _tokenCount = result.tokensAfter:
// the live context-window fill is now the post-compaction count. Derived
Expand Down Expand Up @@ -577,16 +600,6 @@ function isHistoryEntry(pm: ProjectedMessage): boolean {
return pm.source !== 'undo' && pm.source !== 'clear';
}

/** Mirrors agent-core `isRealUserPrompt` (`agent/context/index.ts`): a message
* counts toward an undo only if it is a genuine user prompt. */
function isRealUserPrompt(message: ContextMessage): boolean {
if (message.role !== 'user') return false;
const origin = message.origin;
if (origin === undefined || origin.kind === 'user') return true;
if (origin.kind === 'skill_activation') return origin.trigger === 'user-slash';
return false;
}

/** Single source of truth for the `context.undo` backward walk, shared by both
* projection modes. Mirrors agent-core `undo` (`agent/context/index.ts`): walk
* from the end, skip `origin.kind === 'injection'` (those are KEPT even when
Expand All @@ -612,7 +625,7 @@ function computeUndoCutoff(
if (origin?.kind === 'compaction_summary') break; // stop
removedMessageCount++;
cutoff = i;
if (isRealUserPrompt(messages[i]!.message) && ++removedUserCount >= count) break;
if (isRealUserInput(messages[i]!.message) && ++removedUserCount >= count) break;
}
return { cutoff, removedMessageCount };
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{"type":"metadata","protocol_version":"1.1","created_at":1779256791085}
{"type":"config.update","cwd":"/tmp/work","profileName":"agent","systemPrompt":"You are Kimi.","time":1779256791100}
{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"before compaction"}],"toolCalls":[]},"time":1779256800001}
{"type":"context.apply_compaction","summary":"compacted summary","compactedCount":1,"tokensBefore":100,"tokensAfter":30,"time":1779256800500}
{"type":"context.append_message","message":{"role":"assistant","content":[{"type":"text","text":"assistant reply"}],"toolCalls":[]},"time":1779256800200}
{"type":"context.apply_compaction","summary":"compacted summary","compactedCount":2,"tokensBefore":100,"tokensAfter":30,"time":1779256800500}
{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"after compaction"}],"toolCalls":[]},"time":1779256801000}
Loading
Loading