fix: stop recording resume version metadata (#786)

This commit is contained in:
_Kerman 2026-06-15 21:04:53 +08:00 committed by GitHub
parent 73be7ba17d
commit e10b25f9be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 59 additions and 22 deletions

View file

@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---
Stop writing resume version markers into persisted agent metadata.

View file

@ -86,7 +86,6 @@ export interface AgentOptions {
readonly log?: Logger;
readonly telemetry?: TelemetryClient | undefined;
readonly pluginSessionStarts?: readonly EnabledPluginSessionStart[];
readonly appVersion?: string;
readonly experimentalFlags?: ExperimentalFlagResolver;
}
@ -110,7 +109,6 @@ export class Agent {
readonly hooks?: HookEngine;
readonly log: Logger;
readonly telemetry: TelemetryClient;
readonly appVersion?: string;
readonly experimentalFlags: ExperimentalFlagResolver;
readonly blobStore: BlobStore | undefined;
@ -147,7 +145,6 @@ export class Agent {
this.subagentHost = options.subagentHost;
this.mcp = options.mcp;
this.hooks = options.hookEngine;
this.appVersion = options.appVersion;
this.log = options.log ?? log;
this.telemetry = options.telemetry ?? noopTelemetryClient;
this.experimentalFlags = options.experimentalFlags ?? new FlagResolver();

View file

@ -153,7 +153,6 @@ export class AgentRecords {
type: 'metadata',
protocol_version: AGENT_WIRE_PROTOCOL_VERSION,
created_at: Date.now(),
app_version: this.agent.appVersion,
});
this.metadataInitialized = true;
}
@ -217,20 +216,6 @@ export class AgentRecords {
await this.agent.blobStore.rehydrateParts(msg.content);
}
}
const firstRecord = replayedRecords[0];
if (
firstRecord?.type === 'metadata' &&
firstRecord.app_version !== this.agent.appVersion
) {
this.persistence.append({
type: 'metadata',
protocol_version: AGENT_WIRE_PROTOCOL_VERSION,
created_at: Date.now(),
app_version: this.agent.appVersion,
resumed: true,
});
await this.persistence.flush();
}
return { warning };
}

View file

@ -19,8 +19,6 @@ export interface AgentRecordEvents {
metadata: {
protocol_version: string;
created_at: number;
app_version?: string;
resumed?: boolean;
};
forked: {};

View file

@ -234,6 +234,7 @@ export class Session {
async resume(): Promise<{ warning?: string }> {
await this.skillsReady;
this.log.info('session resume', { app_version: this.options.appVersion });
const { agents } = await this.readMetadata();
this.agents.clear();
// Only the main agent is needed to reopen the session; subagents replay
@ -573,7 +574,6 @@ export class Session {
telemetry: this.telemetry,
log: this.log.createChild({ agentId: id }),
pluginSessionStarts: type === 'main' ? this.options.pluginSessionStarts : undefined,
appVersion: this.options.appVersion,
experimentalFlags: this.experimentalFlags,
});
}

View file

@ -24,6 +24,8 @@ describe('AgentRecords persistence metadata', () => {
type: 'metadata',
protocol_version: AGENT_WIRE_PROTOCOL_VERSION,
});
expect(persistence.records[0]).not.toHaveProperty('app_version');
expect(persistence.records[0]).not.toHaveProperty('resumed');
expect(persistence.records[1]?.type).toBe('turn.prompt');
});

View file

@ -21,6 +21,28 @@ const MOCK_PROVIDER = {
} as const;
describe('Agent resume', () => {
it('does not append metadata when resuming records that include legacy app version', async () => {
const persistence = new RecordingAgentPersistence([
{
type: 'metadata',
protocol_version: AGENT_WIRE_PROTOCOL_VERSION,
created_at: 1,
app_version: '0.0.1-old',
} as unknown as AgentRecord,
{
type: 'turn.prompt',
input: [{ type: 'text', text: 'old prompt' }],
origin: { kind: 'user' },
},
]);
const ctx = testAgent({ persistence });
await ctx.agent.resume();
expect(persistence.appended).toEqual([]);
expect(persistence.records.filter((record) => record.type === 'metadata')).toHaveLength(1);
});
it('replays persisted records without restarting turns, compactions, plan turns, or tools', async () => {
const persistence = new RecordingAgentPersistence(resumeHistory());
const execWithEnv = vi.fn().mockRejectedValue(new Error('Bash should not execute on resume'));

View file

@ -11,6 +11,11 @@ import {
type SDKAPI,
type TelemetryClient,
} from '../../src';
import {
__resetRootLoggerForTest,
getRootLogger,
} from '../../src/logging/logger';
import { resolveLoggingConfig } from '../../src/logging/resolve-config';
import {
recordingContextTelemetry,
type TelemetryContextRecord,
@ -46,6 +51,7 @@ describe('HarnessAPI session model aliases', () => {
});
afterEach(async () => {
await __resetRootLoggerForTest();
await rm(tmp, { recursive: true, force: true });
});
@ -277,6 +283,21 @@ reason = "no rm"
);
});
it('logs app_version when resuming a session', async () => {
await getRootLogger().configure(resolveLoggingConfig({ homeDir }));
const rpc = await createTestRpc();
const created = await rpc.createSession({ workDir, model: 'kimi-code/kimi-for-coding' });
await rpc.closeSession({ sessionId: created.id });
const freshRpc = await createTestRpc({ appVersion: '1.2.3-test' });
await freshRpc.resumeSession({ sessionId: created.id });
await getRootLogger().flushSession(created.id);
const logText = await readFile(join(created.sessionDir, 'logs', 'kimi-code.log'), 'utf-8');
expect(logText).toContain('session resume');
expect(logText).toContain('app_version=1.2.3-test');
});
it('surfaces a config error when a resumed model is configured but unresolvable', async () => {
const rpc = await createTestRpc();
const created = await rpc.createSession({ workDir, model: 'kimi-code/kimi-for-coding' });
@ -397,11 +418,17 @@ max_context_size = 1000000
return join(root, match);
}
async function createTestRpc(options: { readonly telemetry?: TelemetryClient } = {}) {
async function createTestRpc(
options: {
readonly appVersion?: string;
readonly telemetry?: TelemetryClient;
} = {},
) {
const [coreRpc, sdkRpc] = createRPC<CoreAPI, SDKAPI>();
void new KimiCore(coreRpc, {
homeDir,
configPath,
appVersion: options.appVersion,
telemetry: options.telemetry,
});
return sdkRpc({