diff --git a/.changeset/clean-resume-metadata.md b/.changeset/clean-resume-metadata.md new file mode 100644 index 000000000..0e9f89363 --- /dev/null +++ b/.changeset/clean-resume-metadata.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Stop writing resume version markers into persisted agent metadata. diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 424293b4d..1f048db1d 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -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(); diff --git a/packages/agent-core/src/agent/records/index.ts b/packages/agent-core/src/agent/records/index.ts index 8bf050398..c00f169fd 100644 --- a/packages/agent-core/src/agent/records/index.ts +++ b/packages/agent-core/src/agent/records/index.ts @@ -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 }; } diff --git a/packages/agent-core/src/agent/records/types.ts b/packages/agent-core/src/agent/records/types.ts index 835c34465..eb07d10a0 100644 --- a/packages/agent-core/src/agent/records/types.ts +++ b/packages/agent-core/src/agent/records/types.ts @@ -19,8 +19,6 @@ export interface AgentRecordEvents { metadata: { protocol_version: string; created_at: number; - app_version?: string; - resumed?: boolean; }; forked: {}; diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 35efd86cc..7258edfc8 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -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, }); } diff --git a/packages/agent-core/test/agent/records/index.test.ts b/packages/agent-core/test/agent/records/index.test.ts index 12dbf4ec1..5cd039491 100644 --- a/packages/agent-core/test/agent/records/index.test.ts +++ b/packages/agent-core/test/agent/records/index.test.ts @@ -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'); }); diff --git a/packages/agent-core/test/agent/resume.test.ts b/packages/agent-core/test/agent/resume.test.ts index c0f8c776a..26d496902 100644 --- a/packages/agent-core/test/agent/resume.test.ts +++ b/packages/agent-core/test/agent/resume.test.ts @@ -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')); diff --git a/packages/agent-core/test/harness/model-alias-session.test.ts b/packages/agent-core/test/harness/model-alias-session.test.ts index 4b6ffbd5d..5fbbc357e 100644 --- a/packages/agent-core/test/harness/model-alias-session.test.ts +++ b/packages/agent-core/test/harness/model-alias-session.test.ts @@ -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(); void new KimiCore(coreRpc, { homeDir, configPath, + appVersion: options.appVersion, telemetry: options.telemetry, }); return sdkRpc({