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
5 changes: 5 additions & 0 deletions .changeset/steered-contents-origin-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fixed skill instructions injected by the Skill tool showing up as ordinary user messages in the rebuilt transcript.
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export class TranscriptService {
}
const messages = [...reduceContextTranscript(records).entries];
const taskOriginTurnTaskIds = new Set<string>();
const steeredContents = new Map<string, number>();
const steeredContents = new Map<string, Map<string, number>>();
const anchorStack: { taskIdsSnapshot: Set<string> }[] = [];
let anchorFloor = 0;
let sawTurnPrompt = false;
Expand Down Expand Up @@ -495,7 +495,11 @@ export class TranscriptService {
const input = record['input'];
if (Array.isArray(input)) {
const key = JSON.stringify(input);
steeredContents.set(key, (steeredContents.get(key) ?? 0) + 1);
const steerOrigin = (record as { origin?: { kind?: unknown } }).origin?.kind;
const kind = typeof steerOrigin === 'string' ? steerOrigin : 'user';
const byKind = steeredContents.get(key) ?? new Map<string, number>();
byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
steeredContents.set(key, byKind);
}
continue;
}
Expand Down
44 changes: 44 additions & 0 deletions packages/kap-server/test/services/transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2589,6 +2589,50 @@ describe('AgentTranscriptProjector', () => {
}
});

it('readColdSnapshot keeps skill-activation steers as skill markers instead of user frames', async () => {
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-skillsteer-'));
try {
const wireDir = join(home, 'sessions', 'ws', 's1', 'agents', 'main');
await mkdir(wireDir, { recursive: true });
const skillOrigin = {
kind: 'skill_activation',
activationId: 'a1',
skillName: 'write-tui',
trigger: 'model-tool',
skillSource: 'project',
};
const nestedOrigin = { ...skillOrigin, activationId: 'a2', skillName: 'design', trigger: 'nested-skill' };
const skillText = 'Skill tool loaded instructions for this request. Follow them.';
const nestedText = 'Nested skill instructions.';
const records = [
{ type: 'context.append_message', message: { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, time: 1000 },
{ type: 'context.append_message', message: { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, time: 2000 },
{ type: 'turn.steer', input: [{ type: 'text', text: skillText }], origin: skillOrigin, time: 3000 },
{ type: 'context.append_message', message: { role: 'user', content: [{ type: 'text', text: skillText }], toolCalls: [], origin: skillOrigin }, time: 3001 },
{ type: 'turn.steer', input: [{ type: 'text', text: nestedText }], origin: nestedOrigin, time: 3002 },
{ type: 'context.append_message', message: { role: 'user', content: [{ type: 'text', text: nestedText }], toolCalls: [], origin: nestedOrigin }, time: 3003 },
{ type: 'context.append_message', message: { role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] }, time: 4000 },
];
await writeFile(join(wireDir, 'wire.jsonl'), `${records.map((r) => JSON.stringify(r)).join('\n')}\n`);

const snapshot = await coldTranscriptService(home).readColdSnapshot('s1', 'main');
const markers = snapshot!.items.filter((item) => item.kind === 'marker');
expect(markers).toHaveLength(2);
expect(markers.every((item) => item.kind === 'marker' && item.marker === 'skill')).toBe(true);
const turns = snapshot!.items.filter((item) => item.kind === 'turn');
expect(turns).toHaveLength(1);
const turn = turns[0];
if (turn?.kind !== 'turn') throw new Error('expected turn');
expect(turn.prompt).toBe('active');
const userFrames = turn.steps
.flatMap((step) => step.frames)
.filter((frame) => frame.kind === 'text' && frame.role === 'user');
expect(userFrames).toHaveLength(0);
} finally {
await rm(home, { recursive: true, force: true });
}
});

it('readColdSnapshot drops undone task-turn boundaries so a redelivered notification folds', async () => {
const home = await mkdtemp(join(tmpdir(), 'transcript-cold-undoboundary-'));
try {
Expand Down
20 changes: 14 additions & 6 deletions packages/transcript/src/history/groupTurns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ export function groupMessagesIntoSnapshot(
messages: readonly HistoryMessage[],
options?: {
readonly taskOriginTurnTaskIds?: ReadonlySet<string>;
readonly steeredContents?: ReadonlyMap<string, number>;
readonly steeredContents?: ReadonlyMap<string, ReadonlyMap<string, number>>;
},
): AgentTranscriptSnapshot {
const items: TranscriptItem[] = [];
const attachments: TranscriptAttachment[] = [];
const steeredContents = new Map(options?.steeredContents ?? []);
const steeredContents = new Map(
[...(options?.steeredContents ?? [])].map(([key, byKind]) => [key, new Map(byKind)]),
);
let turn: TurnDraft | undefined;
let pendingNotificationFrames: {
text: string;
Expand Down Expand Up @@ -217,10 +219,17 @@ export function groupMessagesIntoSnapshot(
}
continue;
}
const markerKey = originKind !== undefined ? MARKER_USER_ORIGINS[originKind] : undefined;
if (markerKey !== undefined && !isUserSlashPrompt(message)) {
pushMarker(markerKey, { text: textOf(message), origin: message.origin });
continue;
Comment thread
kimi-agent-bot marked this conversation as resolved.
}
const contentKey = JSON.stringify(message.content ?? []);
const steeredRemaining = steeredContents.get(contentKey) ?? 0;
if (steeredRemaining > 0) {
steeredContents.set(contentKey, steeredRemaining - 1);
const steerKind = originKind ?? 'user';
const steeredByKind = steeredContents.get(contentKey);
const steeredRemaining = steeredByKind?.get(steerKind) ?? 0;
if (steeredByKind !== undefined && steeredRemaining > 0) {
steeredByKind.set(steerKind, steeredRemaining - 1);
const bundled = bundledSkillActivations(message);
const parts = message.content ?? [];
bundled.forEach((activation, index) => {
Expand All @@ -239,7 +248,6 @@ export function groupMessagesIntoSnapshot(
});
continue;
}
const markerKey = originKind !== undefined ? MARKER_USER_ORIGINS[originKind] : undefined;
if (markerKey !== undefined) {
const opening = isUserSlashPrompt(message) ? foldTurnOpeningInput(message) : undefined;
pushMarker(markerKey, { text: opening?.text ?? textOf(message), origin: message.origin });
Expand Down
141 changes: 137 additions & 4 deletions packages/transcript/test/layers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ describe('groupMessagesIntoSnapshot (cold path)', () => {
{ role: 'user', content: [{ type: 'text', text: 'steered in' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), 1]]) },
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) },
);

expect(snapshot.items.map((i) => i.kind)).toEqual(['turn']);
Expand All @@ -527,7 +527,7 @@ describe('groupMessagesIntoSnapshot (cold path)', () => {
{ role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] },
{ role: 'user', content: [{ type: 'text', text: 'steered in' }], toolCalls: [], origin: { kind: 'user' } },
],
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), 1]]) },
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) },
);

expect(snapshot.items.map((i) => i.kind)).toEqual(['turn']);
Expand All @@ -550,7 +550,7 @@ describe('groupMessagesIntoSnapshot (cold path)', () => {
{ role: 'user', content: [{ type: 'text', text: 'next question' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), 1]]) },
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) },
);

const turns = snapshot.items.filter((i) => i.kind === 'turn');
Expand All @@ -574,7 +574,7 @@ describe('groupMessagesIntoSnapshot (cold path)', () => {
{ role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] },
{ role: 'user', content: [{ type: 'text', text: 'plain follow-up' }], toolCalls: [], origin: { kind: 'user' } },
],
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), 1]]) },
{ steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) },
);

expect(snapshot.items.map((i) => i.kind)).toEqual(['turn', 'turn']);
Expand Down Expand Up @@ -1099,6 +1099,139 @@ describe('groupMessagesIntoSnapshot (cold path)', () => {
expect(slashTurn.steps).toHaveLength(2);
});

it('keeps model-tool skill activations as markers even when their content matches a steer record', () => {
const skillContent = [{ type: 'text', text: 'skill body' }];
const snapshot = groupMessagesIntoSnapshot(
[
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] },
{
role: 'user',
content: skillContent,
toolCalls: [],
origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'write-tui' } as {
kind: string;
},
},
{ role: 'assistant', content: [{ type: 'text', text: 'used the skill' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify(skillContent), new Map([['skill_activation', 1]])]]) },
);

expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker']);
const marker = snapshot.items[1];
if (marker?.kind !== 'marker') throw new Error('expected marker');
expect(marker.marker).toBe('skill');
});

it('still folds cron-origin steers into the running turn by content match', () => {
const cronContent = [{ type: 'text', text: 'cron tick' }];
const snapshot = groupMessagesIntoSnapshot(
[
{ role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] },
{
role: 'user',
content: cronContent,
toolCalls: [],
origin: { kind: 'cron_job', jobId: 'job1' } as { kind: string },
},
{ role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify(cronContent), new Map([['cron_job', 1]])]]) },
);

expect(snapshot.items.map((item) => item.kind)).toEqual(['turn']);
const turn = snapshot.items[0];
if (turn?.kind !== 'turn') throw new Error('expected turn');
expect(turn.steps).toHaveLength(2);
expect(turn.steps[1]?.frames[0]).toMatchObject({
kind: 'text',
role: 'user',
text: 'cron tick',
});
});

it('still folds user-slash skill activations into the running turn by content match', () => {
const slashContent = [{ type: 'text', text: 'slash skill body' }];
const snapshot = groupMessagesIntoSnapshot(
[
{ role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] },
{
role: 'user',
content: slashContent,
toolCalls: [],
origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'gen-docs' } as {
kind: string;
},
},
{ role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify(slashContent), new Map([['skill_activation', 1]])]]) },
);

expect(snapshot.items.map((item) => item.kind)).toEqual(['turn']);
const turn = snapshot.items[0];
if (turn?.kind !== 'turn') throw new Error('expected turn');
expect(turn.steps).toHaveLength(2);
expect(turn.steps[1]?.frames[0]).toMatchObject({
kind: 'text',
role: 'user',
text: 'slash skill body',
});
});

it('consumes the steer count for marker-only activations so a later identical prompt opens its own turn', () => {
const shared = [{ type: 'text', text: 'same text' }];
const snapshot = groupMessagesIntoSnapshot(
[
{ role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] },
{
role: 'user',
content: shared,
toolCalls: [],
origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'x' } as {
kind: string;
},
},
{ role: 'user', content: shared, toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify(shared), new Map([['skill_activation', 1]])]]) },
);

expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker', 'turn']);
});

it('does not consume a user steer count for a compaction summary with identical content', () => {
const shared = [{ type: 'text', text: 'same text' }];
const snapshot = groupMessagesIntoSnapshot(
[
{ role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] },
{
role: 'user',
content: shared,
toolCalls: [],
origin: { kind: 'compaction_summary' } as { kind: string },
},
{ role: 'user', content: shared, toolCalls: [], origin: { kind: 'user' } },
{ role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] },
],
{ steeredContents: new Map([[JSON.stringify(shared), new Map([['user', 1]])]]) },
);

expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker']);
const turn = snapshot.items[0];
if (turn?.kind !== 'turn') throw new Error('expected turn');
const steered = turn.steps
.flatMap((step) => step.frames)
.filter((frame) => frame.kind === 'text' && frame.role === 'user');
expect(steered).toHaveLength(1);
});

it('starts a promptless turn for turn-opening system triggers (goal continuation)', () => {
const snapshot = groupMessagesIntoSnapshot([
{ role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } },
Expand Down
Loading