mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix: align v2 compaction auth guards
This commit is contained in:
parent
e3fb6d249e
commit
ca237a0264
4 changed files with 76 additions and 18 deletions
|
|
@ -115,7 +115,7 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IAgentWireService private readonly wire: IWireService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
@IAgentTurnService turnService: IAgentTurnService,
|
||||
@IAgentTurnService private readonly turn: IAgentTurnService,
|
||||
@IAgentLoopService loopService: IAgentLoopService,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -158,6 +158,12 @@ export class AgentFullCompactionService extends Disposable implements IAgentFull
|
|||
if (this.compactionCountInTurn > this.strategy.maxCompactionPerTurn) return false;
|
||||
|
||||
const history = this.context.get();
|
||||
if (data.source === 'manual' && this.turn.getActiveTurn() !== undefined) {
|
||||
throw new KimiError(
|
||||
ErrorCodes.COMPACTION_UNABLE,
|
||||
'Cannot compact while a turn is active. Wait for it to finish, then retry.',
|
||||
);
|
||||
}
|
||||
const tokenCount = estimateTokensForMessages(history);
|
||||
const compactedCount = this.strategy.computeCompactCount(history, data.source);
|
||||
if (compactedCount === 0) {
|
||||
|
|
|
|||
|
|
@ -285,7 +285,9 @@ export class ModelResolverService extends Disposable implements IModelResolver {
|
|||
async getAuth(options): Promise<ProviderRequestAuth | undefined> {
|
||||
const tokenProvider = oauthService.resolveTokenProvider(providerKey, oauthRef);
|
||||
if (tokenProvider === undefined) throw loginRequired();
|
||||
const apiKey = await tokenProvider.getAccessToken({ force: options?.force ?? false });
|
||||
const apiKey = await tokenProvider.getAccessToken(
|
||||
options?.force === true ? { force: true } : undefined,
|
||||
);
|
||||
if (apiKey.trim().length === 0) throw loginRequired();
|
||||
return { apiKey };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { microCompactionFlag } from '#/agent/microCompaction/flag';
|
|||
import { estimateTokensForMessages } from '#/_base/utils/tokens';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs';
|
||||
import type { TestAgentContext, TestAgentOptions, TestAgentServiceOverride } from '../harness';
|
||||
import { appServices, testAgent } from '../harness';
|
||||
import { appServices, createCommandRunner, execEnvServices, testAgent } from '../harness';
|
||||
import {
|
||||
IAgentFullCompactionService,
|
||||
IAgentMicroCompactionService,
|
||||
|
|
@ -27,6 +27,7 @@ import {
|
|||
IAgentProfileService,
|
||||
ISessionTodoService,
|
||||
} from '#/index';
|
||||
import { IAgentTurnService } from '#/agent/turn/turn';
|
||||
|
||||
type GenerateFn = NonNullable<TestAgentOptions['generate']>;
|
||||
|
||||
|
|
@ -273,7 +274,7 @@ describe('FullCompaction', () => {
|
|||
compacted_count: 6,
|
||||
retry_count: 0,
|
||||
thinking_level: 'off',
|
||||
input_other: 520,
|
||||
input_other: 1181,
|
||||
output: 8,
|
||||
input_cache_read: 0,
|
||||
input_cache_creation: 0,
|
||||
|
|
@ -282,6 +283,36 @@ describe('FullCompaction', () => {
|
|||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
it('rejects a manual compaction while a turn is active', async () => {
|
||||
const ctx = testAgent(execEnvServices({ processRunner: createCommandRunner('should-not-run') }));
|
||||
ctx.configure({
|
||||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
tools: ['Bash'],
|
||||
});
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.mockNextResponse({ type: 'text', text: 'I will wait for approval.' }, bashCall());
|
||||
|
||||
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Start the active turn' }] });
|
||||
const approval = await ctx.takeApprovalRequest();
|
||||
expect(ctx.get(IAgentTurnService).getActiveTurn()).toBeDefined();
|
||||
|
||||
await expect(ctx.rpc.beginCompaction({})).rejects.toMatchObject({
|
||||
code: 'compaction.unable',
|
||||
message: 'Cannot compact while a turn is active. Wait for it to finish, then retry.',
|
||||
});
|
||||
const events = ctx.newEvents();
|
||||
expect(eventIndex(events, 'full_compaction.begin')).toBe(-1);
|
||||
expect(eventIndex(events, 'compaction.started')).toBe(-1);
|
||||
expect(ctx.get(IAgentFullCompactionService).compacting).toBeNull();
|
||||
expect(ctx.llmCalls).toHaveLength(1);
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Turn done.' });
|
||||
approval.respond({ decision: 'rejected', selectedLabel: 'reject' });
|
||||
await ctx.untilTurnEnd();
|
||||
expect(ctx.get(IAgentTurnService).getActiveTurn()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('projects the compacted prefix before sending the summary request', async () => {
|
||||
const ctx = testAgent();
|
||||
ctx.configure({
|
||||
|
|
@ -434,7 +465,12 @@ describe('FullCompaction', () => {
|
|||
expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token', 'fresh-token']);
|
||||
expect(tokenCalls).toEqual([undefined, true, undefined]);
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Recovered compacted summary.' },
|
||||
{ role: 'user', text: 'old user one' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{
|
||||
role: 'user',
|
||||
text: expect.stringContaining('Recovered compacted summary.'),
|
||||
},
|
||||
]);
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
|
@ -1595,7 +1631,12 @@ describe('FullCompaction', () => {
|
|||
|
||||
expect(ctx.llmCalls).toHaveLength(1);
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Compacted after no-op cancel.' },
|
||||
{ role: 'user', text: 'old user one' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{
|
||||
role: 'user',
|
||||
text: expect.stringContaining('Compacted after no-op cancel.'),
|
||||
},
|
||||
]);
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
|
@ -2490,6 +2531,15 @@ function textMessage(role: 'user' | 'assistant', text: string): Message {
|
|||
};
|
||||
}
|
||||
|
||||
function bashCall(): ToolCall {
|
||||
return {
|
||||
type: 'function',
|
||||
id: 'call_bash',
|
||||
name: 'Bash',
|
||||
arguments: JSON.stringify({ command: 'printf should-not-run', timeout: 60 }),
|
||||
};
|
||||
}
|
||||
|
||||
function messageText(message: Message | undefined): string {
|
||||
return message?.content.map((part) => (part.type === 'text' ? part.text : '')).join('') ?? '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,12 +299,12 @@ describe('ModelResolverService', () => {
|
|||
|
||||
it('force-refreshes OAuth credentials and replays a request after 401', async () => {
|
||||
configureOAuthModel();
|
||||
const tokenCalls: boolean[] = [];
|
||||
const tokenCalls: Array<boolean | undefined> = [];
|
||||
const authKeys: string[] = [];
|
||||
resolveTokenProvider.mockReturnValue({
|
||||
getAccessToken: async (options: { readonly force: boolean }) => {
|
||||
tokenCalls.push(options.force);
|
||||
return options.force ? 'forced-refresh-token' : 'fresh-token';
|
||||
getAccessToken: async (options?: { readonly force?: boolean }) => {
|
||||
tokenCalls.push(options?.force);
|
||||
return options?.force === true ? 'forced-refresh-token' : 'fresh-token';
|
||||
},
|
||||
});
|
||||
generateImpl = async (_system, _tools, _history, options) => {
|
||||
|
|
@ -333,7 +333,7 @@ describe('ModelResolverService', () => {
|
|||
}
|
||||
|
||||
expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token']);
|
||||
expect(tokenCalls).toEqual([false, true]);
|
||||
expect(tokenCalls).toEqual([undefined, true]);
|
||||
expect(events).toContainEqual({ type: 'part', part: { type: 'text', text: 'recovered' } });
|
||||
});
|
||||
|
||||
|
|
@ -341,8 +341,8 @@ describe('ModelResolverService', () => {
|
|||
configureOAuthModel();
|
||||
const authKeys: string[] = [];
|
||||
resolveTokenProvider.mockReturnValue({
|
||||
getAccessToken: async (options: { readonly force: boolean }) =>
|
||||
options.force ? 'forced-refresh-token' : 'fresh-token',
|
||||
getAccessToken: async (options?: { readonly force?: boolean }) =>
|
||||
options?.force === true ? 'forced-refresh-token' : 'fresh-token',
|
||||
});
|
||||
generateImpl = async (_system, _tools, _history, options) => {
|
||||
authKeys.push(options?.auth?.apiKey ?? '<missing>');
|
||||
|
|
@ -393,12 +393,12 @@ describe('ModelResolverService', () => {
|
|||
|
||||
it('force-refreshes OAuth credentials and replays video upload after 401', async () => {
|
||||
configureOAuthModel();
|
||||
const tokenCalls: boolean[] = [];
|
||||
const tokenCalls: Array<boolean | undefined> = [];
|
||||
const authKeys: string[] = [];
|
||||
resolveTokenProvider.mockReturnValue({
|
||||
getAccessToken: async (options: { readonly force: boolean }) => {
|
||||
tokenCalls.push(options.force);
|
||||
return options.force ? 'forced-refresh-token' : 'fresh-token';
|
||||
getAccessToken: async (options?: { readonly force?: boolean }) => {
|
||||
tokenCalls.push(options?.force);
|
||||
return options?.force === true ? 'forced-refresh-token' : 'fresh-token';
|
||||
},
|
||||
});
|
||||
uploadVideoImpl = async (_input, options) => {
|
||||
|
|
@ -416,7 +416,7 @@ describe('ModelResolverService', () => {
|
|||
videoUrl: { url: 'https://example.test/video' },
|
||||
});
|
||||
expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token']);
|
||||
expect(tokenCalls).toEqual([false, true]);
|
||||
expect(tokenCalls).toEqual([undefined, true]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue