mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix: warn instead of error on newer wire protocol version (#49)
* fix: warn instead of error on newer wire protocol version * fix * fix test
This commit is contained in:
parent
064343a6e5
commit
cf2227e8a5
14 changed files with 72 additions and 22 deletions
7
.changeset/warn-newer-wire-version.md
Normal file
7
.changeset/warn-newer-wire-version.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
"@moonshot-ai/kimi-code-sdk": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Resume sessions with a newer wire protocol version instead of failing. A warning is now shown in the TUI and records are replayed without migration.
|
||||
|
|
@ -88,6 +88,7 @@ import type {
|
|||
TurnStepCompletedEvent,
|
||||
TurnStepInterruptedEvent,
|
||||
TurnStepStartedEvent,
|
||||
WarningEvent,
|
||||
} from '@moonshot-ai/kimi-code-sdk';
|
||||
import chalk from 'chalk';
|
||||
|
||||
|
|
@ -802,6 +803,10 @@ export class KimiTUI {
|
|||
this.requireSession(),
|
||||
);
|
||||
}
|
||||
const resumeState = this.session?.getResumeState();
|
||||
if (resumeState?.warning !== undefined) {
|
||||
this.showStatus(`Warning: ${resumeState.warning}`, this.state.theme.colors.warning);
|
||||
}
|
||||
if (this.session !== undefined) {
|
||||
this.startSessionEventSubscription();
|
||||
}
|
||||
|
|
@ -2102,6 +2107,10 @@ export class KimiTUI {
|
|||
} finally {
|
||||
this.startSessionEventSubscription();
|
||||
}
|
||||
const resumeState = session.getResumeState();
|
||||
if (resumeState?.warning !== undefined) {
|
||||
this.showStatus(`Warning: ${resumeState.warning}`, this.state.theme.colors.warning);
|
||||
}
|
||||
this.showStatus(statusMessage);
|
||||
}
|
||||
|
||||
|
|
@ -2260,6 +2269,9 @@ export class KimiTUI {
|
|||
case 'error':
|
||||
this.handleSessionError(event);
|
||||
break;
|
||||
case 'warning':
|
||||
this.handleSessionWarning(event);
|
||||
break;
|
||||
case 'compaction.started':
|
||||
this.handleCompactionBegin(event);
|
||||
break;
|
||||
|
|
@ -2680,6 +2692,10 @@ export class KimiTUI {
|
|||
}
|
||||
}
|
||||
|
||||
private handleSessionWarning(event: WarningEvent): void {
|
||||
this.showStatus(`Warning: ${event.message}`, this.state.theme.colors.warning);
|
||||
}
|
||||
|
||||
private renderMcpServerStatus(server: McpServerStatusSnapshot): void {
|
||||
const key = mcpServerStatusKey(server);
|
||||
if (this.state.renderedMcpServerStatusKeys.get(server.name) === key) return;
|
||||
|
|
|
|||
|
|
@ -234,11 +234,12 @@ export class Agent {
|
|||
this.tools.setActiveTools(profile.tools);
|
||||
}
|
||||
|
||||
async resume(): Promise<void> {
|
||||
await this.records.replay();
|
||||
async resume(): Promise<{ warning?: string }> {
|
||||
const result = await this.records.replay();
|
||||
await this.background.loadFromDisk();
|
||||
await this.background.reconcile();
|
||||
this.turn.finishResume();
|
||||
return result;
|
||||
}
|
||||
|
||||
get rpcMethods(): PromisableMethods<AgentAPI> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Agent } from '..';
|
||||
import {
|
||||
AGENT_WIRE_PROTOCOL_VERSION,
|
||||
isNewerWireVersion,
|
||||
migrateWireRecord,
|
||||
resolveWireMigrations,
|
||||
type WireMigration,
|
||||
|
|
@ -140,11 +141,12 @@ export class AgentRecords {
|
|||
}
|
||||
}
|
||||
|
||||
async replay(): Promise<void> {
|
||||
async replay(): Promise<{ warning?: string }> {
|
||||
if (!this.persistence) throw new Error('No persistence provided for AgentRecords');
|
||||
let migrations: readonly WireMigration[] = [];
|
||||
let hasMetadata = false;
|
||||
let shouldRewrite = false;
|
||||
let warning: string | undefined;
|
||||
const replayedRecords: AgentRecord[] = [];
|
||||
for await (const record of this.persistence.read()) {
|
||||
if (!hasMetadata) {
|
||||
|
|
@ -154,8 +156,13 @@ export class AgentRecords {
|
|||
hasMetadata = true;
|
||||
this.metadataInitialized = true;
|
||||
const readVersion = record.protocol_version;
|
||||
migrations = resolveWireMigrations(readVersion);
|
||||
shouldRewrite = readVersion !== AGENT_WIRE_PROTOCOL_VERSION;
|
||||
if (isNewerWireVersion(readVersion)) {
|
||||
warning = `Session wire protocol version ${readVersion} is newer than the current version ${AGENT_WIRE_PROTOCOL_VERSION}. Records will be replayed without migration.`;
|
||||
shouldRewrite = false;
|
||||
} else {
|
||||
migrations = resolveWireMigrations(readVersion);
|
||||
shouldRewrite = readVersion !== AGENT_WIRE_PROTOCOL_VERSION;
|
||||
}
|
||||
}
|
||||
let migratedRecord = migrateWireRecord(
|
||||
record as WireMigrationRecord,
|
||||
|
|
@ -174,6 +181,7 @@ export class AgentRecords {
|
|||
this.persistence.rewrite(replayedRecords);
|
||||
await this.persistence.flush();
|
||||
}
|
||||
return { warning };
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -16,15 +16,14 @@ export interface WireMigration {
|
|||
|
||||
const MIGRATIONS: readonly WireMigration[] = [migrateV1_0ToV1_1];
|
||||
|
||||
export function isNewerWireVersion(readVersion: string): boolean {
|
||||
return compareWireVersions(readVersion, AGENT_WIRE_PROTOCOL_VERSION) > 0;
|
||||
}
|
||||
|
||||
export function resolveWireMigrations(readVersion: string): readonly WireMigration[] {
|
||||
if (compareWireVersions(readVersion, AGENT_WIRE_PROTOCOL_VERSION) === 0) {
|
||||
if (compareWireVersions(readVersion, AGENT_WIRE_PROTOCOL_VERSION) >= 0) {
|
||||
return [];
|
||||
}
|
||||
if (compareWireVersions(readVersion, AGENT_WIRE_PROTOCOL_VERSION) > 0) {
|
||||
throw new Error(
|
||||
`Unsupported wire protocol version: ${readVersion} (current: ${AGENT_WIRE_PROTOCOL_VERSION})`,
|
||||
);
|
||||
}
|
||||
|
||||
const migrations: WireMigration[] = [];
|
||||
let version = readVersion;
|
||||
|
|
|
|||
|
|
@ -252,8 +252,10 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
|
|||
telemetry: withTelemetryContext(this.telemetry, { sessionId: summary.id }),
|
||||
initializeMainAgent: false,
|
||||
});
|
||||
let warning: string | undefined;
|
||||
try {
|
||||
await session.resume();
|
||||
const resumeResult = await session.resume();
|
||||
warning = resumeResult.warning;
|
||||
await this.refreshSessionRuntimeConfig(session, config);
|
||||
} catch (error) {
|
||||
await session.close().catch(() => {});
|
||||
|
|
@ -263,7 +265,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
|
|||
throw error;
|
||||
}
|
||||
this.sessions.set(summary.id, session);
|
||||
return resumeSessionResult(summary, session);
|
||||
return resumeSessionResult(summary, session, warning);
|
||||
}
|
||||
|
||||
async forkSession(input: ForkSessionPayload): Promise<ResumeSessionResult> {
|
||||
|
|
@ -693,6 +695,7 @@ function telemetryErrorReason(error: unknown): string {
|
|||
async function resumeSessionResult(
|
||||
summary: SessionSummary,
|
||||
session: Session,
|
||||
warning?: string,
|
||||
): Promise<ResumeSessionResult> {
|
||||
const api = new SessionAPIImpl(session);
|
||||
const agents: Record<string, ResumedAgentState> = {};
|
||||
|
|
@ -719,6 +722,7 @@ async function resumeSessionResult(
|
|||
...summary,
|
||||
sessionMetadata: api.getSessionMetadata({}),
|
||||
agents,
|
||||
warning,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,8 +69,12 @@ export interface SkillActivatedEvent {
|
|||
|
||||
export interface ErrorEvent extends KimiErrorPayload {
|
||||
readonly type: 'error';
|
||||
readonly sessionId?: string | undefined;
|
||||
readonly agentId?: string | undefined;
|
||||
}
|
||||
|
||||
export interface WarningEvent {
|
||||
readonly type: 'warning';
|
||||
readonly message: string;
|
||||
readonly code?: string | undefined;
|
||||
}
|
||||
|
||||
export interface TurnStartedEvent {
|
||||
|
|
@ -265,6 +269,7 @@ export interface McpServerStatusPayload {
|
|||
|
||||
export type AgentEvent =
|
||||
| ErrorEvent
|
||||
| WarningEvent
|
||||
| AgentStatusUpdatedEvent
|
||||
| SessionMetaUpdatedEvent
|
||||
| SkillActivatedEvent
|
||||
|
|
|
|||
|
|
@ -36,4 +36,5 @@ export interface ResumedAgentState {
|
|||
export interface ResumeSessionResult extends SessionSummary {
|
||||
readonly sessionMetadata: SessionMeta;
|
||||
readonly agents: Readonly<Record<string, ResumedAgentState>>;
|
||||
readonly warning?: string | undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,15 +148,20 @@ export class Session {
|
|||
return agent;
|
||||
}
|
||||
|
||||
async resume() {
|
||||
async resume(): Promise<{ warning?: string }> {
|
||||
await this.skillsReady;
|
||||
const { agents } = await this.readMetadata();
|
||||
this.agents.clear();
|
||||
let warning: string | undefined;
|
||||
const resumeTasks = Object.keys(agents).map(async (id) => {
|
||||
const agent = this.ensureResumeAgentInstantiated(id, agents);
|
||||
await agent.resume();
|
||||
const result = await agent.resume();
|
||||
if (result.warning !== undefined && warning === undefined) {
|
||||
warning = result.warning;
|
||||
}
|
||||
});
|
||||
await Promise.all(resumeTasks);
|
||||
const resumeWarning = warning;
|
||||
// A session migrated from an external tool ships a wire without the
|
||||
// `config.update` bootstrap events a natively-created agent writes, so the
|
||||
// main agent comes back with an empty system prompt and no tools. Apply the
|
||||
|
|
@ -168,6 +173,7 @@ export class Session {
|
|||
await this.bootstrapAgentProfile(main, profile);
|
||||
}
|
||||
await this.triggerSessionStart('resume');
|
||||
return { warning: resumeWarning };
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ describe('AgentRecords persistence metadata', () => {
|
|||
expect(migrated.message.toolCalls[0]?.['function']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects replaying records from a newer wire version', async () => {
|
||||
it('warns but continues when replaying records from a newer wire version', async () => {
|
||||
const persistence = new InMemoryAgentRecordPersistence([
|
||||
{
|
||||
type: 'metadata',
|
||||
|
|
@ -167,9 +167,9 @@ describe('AgentRecords persistence metadata', () => {
|
|||
]);
|
||||
const records = new AgentRecords(() => {}, persistence);
|
||||
|
||||
await expect(records.replay()).rejects.toThrow(
|
||||
`Unsupported wire protocol version: 9.9 (current: ${AGENT_WIRE_PROTOCOL_VERSION})`,
|
||||
);
|
||||
const result = await records.replay();
|
||||
expect(result.warning).toContain('9.9');
|
||||
expect(result.warning).toContain(AGENT_WIRE_PROTOCOL_VERSION);
|
||||
});
|
||||
|
||||
it('rejects replaying records without a registered migration path', async () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export type {
|
|||
SessionMetaUpdatedEvent,
|
||||
SkillActivatedEvent,
|
||||
ErrorEvent,
|
||||
WarningEvent,
|
||||
UsageStatus,
|
||||
} from '@moonshot-ai/agent-core';
|
||||
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ function resumeStateFromSummary(
|
|||
return {
|
||||
sessionMetadata: summary.sessionMetadata,
|
||||
agents: summary.agents,
|
||||
warning: summary.warning,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,6 @@ export interface SessionSummary {
|
|||
readonly metadata?: JsonObject | undefined;
|
||||
}
|
||||
|
||||
export type ResumedSessionState = Pick<ResumeSessionResult, 'sessionMetadata' | 'agents'>;
|
||||
export type ResumedSessionState = Pick<ResumeSessionResult, 'sessionMetadata' | 'agents' | 'warning'>;
|
||||
|
||||
export interface ResumedSessionSummary extends SessionSummary, ResumedSessionState {}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ describe('Event public types', () => {
|
|||
case 'session.meta.updated':
|
||||
case 'skill.activated':
|
||||
case 'error':
|
||||
case 'warning':
|
||||
case 'turn.started':
|
||||
case 'turn.ended':
|
||||
case 'turn.step.started':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue