Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/agent-core-mcp-hash-and-injection-compaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kimi-code": patch
"@moonshot-ai/kimi-code-sdk": patch
---

Fix two agent-core edge cases: long MCP tool names now always get an 8-char hex hash suffix (a signed-hash bug could emit a 9-char `-xxxxxxxx` suffix), and an injected system reminder that gets folded into a compaction summary is now cleared instead of being pinned to the summary message — so plugin session-start blocks re-inject and plan-mode reminders stay at full strength after compaction.
8 changes: 7 additions & 1 deletion packages/agent-core/src/agent/injection/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ export abstract class DynamicInjector {

onContextCompacted(compactedCount: number): void {
if (this.injectedAt !== null) {
// applyCompaction replaces the first `compactedCount` messages with a
// single summary at index 0, so a surviving injection (old index >=
// compactedCount) maps to new index >= 1. An injection that was inside
// the compacted prefix — including the last one (injectedAt ===
// compactedCount - 1, which yields 0) — was folded into the summary and
// must become null rather than pointing at the summary itself.
const newInjectedAt = this.injectedAt - compactedCount + 1;
this.injectedAt = newInjectedAt >= 0 ? newInjectedAt : null;
this.injectedAt = newInjectedAt >= 1 ? newInjectedAt : null;
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/agent-core/src/mcp/tool-naming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@ function stableHash8(input: string): string {
hash ^= input.codePointAt(i)!;
hash = Math.trunc(Math.imul(hash, 0x01000193));
}
return hash.toString(16).padStart(8, '0');
// `Math.imul` yields a signed 32-bit int, so coerce to unsigned before
// hex-encoding — otherwise a negative hash renders as a 9-char `-xxxxxxxx`
// suffix (the `-` is not a hex digit), breaking the documented 8-char hash.
const unsigned = hash < 0 ? hash + 0x1_0000_0000 : hash;
return unsigned.toString(16).padStart(8, '0');
}
38 changes: 38 additions & 0 deletions packages/agent-core/test/agent/injection/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ class BoomInjector extends DynamicInjector {
}
}

class ProbeInjector extends DynamicInjector {
override readonly injectionVariant = 'probe_test';
protected override getInjection(): string | undefined {
return undefined;
}
setInjectedAt(value: number | null): void {
(this as unknown as { injectedAt: number | null }).injectedAt = value;
}
getInjectedAt(): number | null {
return (this as unknown as { injectedAt: number | null }).injectedAt;
}
}

function installInjectors(manager: InjectionManager, injectors: DynamicInjector[]): void {
(manager as unknown as { injectors: DynamicInjector[] }).injectors = injectors;
}
Expand Down Expand Up @@ -112,3 +125,28 @@ describe('InjectionManager registration', () => {
expect(injectors.some((injector) => injector instanceof TodoListReminderInjector)).toBe(true);
});
});

describe('DynamicInjector.onContextCompacted index remapping', () => {
it('remaps a surviving injection to its post-summary index', () => {
const ctx = testAgent();
const probe = new ProbeInjector(ctx.agent);
probe.setInjectedAt(5); // old index 5
probe.onContextCompacted(3); // first 3 messages folded into the summary at index 0
expect(probe.getInjectedAt()).toBe(3); // 5 - 3 + 1
});

it('nulls an injection folded into the summary, including the last compacted message', () => {
const ctx = testAgent();
// Boundary: injectedAt === compactedCount - 1 was previously remapped to 0
// (pointing at the summary) instead of null.
const last = new ProbeInjector(ctx.agent);
last.setInjectedAt(2);
last.onContextCompacted(3); // indices 0..2 compacted away
expect(last.getInjectedAt()).toBeNull();

const earlier = new ProbeInjector(ctx.agent);
earlier.setInjectedAt(0);
earlier.onContextCompacted(3);
expect(earlier.getInjectedAt()).toBeNull();
});
});
5 changes: 5 additions & 0 deletions packages/agent-core/test/mcp/tool-naming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ describe('qualifyMcpToolName', () => {
const name = qualifyMcpToolName(server, tool);
expect(name.length).toBeLessThanOrEqual(64);
expect(name.startsWith('mcp__')).toBe(true);
// The suffix must be a deterministic 8-char lowercase-hex hash. `Math.imul`
// yields a signed int, so a negative hash must not leak a `-` sign (which
// would also make the suffix 9 chars). These inputs hash negative.
const suffix = name.slice(name.lastIndexOf('_') + 1);
expect(suffix).toMatch(/^[0-9a-f]{8}$/);
// Same input → same output (stable hash).
expect(qualifyMcpToolName(server, tool)).toBe(name);
});
Expand Down