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:
_Kerman 2026-05-26 14:56:32 +08:00 committed by GitHub
parent 064343a6e5
commit cf2227e8a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 72 additions and 22 deletions

View 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.

View file

@ -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;

View file

@ -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> {

View file

@ -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> {

View file

@ -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;

View file

@ -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,
};
}

View file

@ -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

View file

@ -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;
}

View file

@ -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> {

View file

@ -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 () => {

View file

@ -16,6 +16,7 @@ export type {
SessionMetaUpdatedEvent,
SkillActivatedEvent,
ErrorEvent,
WarningEvent,
UsageStatus,
} from '@moonshot-ai/agent-core';

View file

@ -395,6 +395,7 @@ function resumeStateFromSummary(
return {
sessionMetadata: summary.sessionMetadata,
agents: summary.agents,
warning: summary.warning,
};
}

View file

@ -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 {}

View file

@ -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':