Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/file-history-always-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Turn-level file history is now always on; the experimental file-history flag has been removed.
Comment thread
wbxl2000 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export interface FileHistoryContent {
export interface IAgentFileHistoryService {
readonly _serviceBrand: undefined;

enabled(): boolean;
history(): FileHistoryState;
settled(): Promise<void>;
captureForActiveTurn(path: string): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Feature } from '#/features/feature';
import { registerFeature } from '#/features/featureRegistry';

import './flag';
import { IAgentFileHistoryService } from './fileHistory';
import { AgentFileHistoryService } from './fileHistoryService';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { IHostFileSystem } from '#/os/interface/hostFileSystem';
import { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore';
import { TurnEnded } from '#/agent/loop/turnOps';
import { IEventBus } from '#/app/event/eventBus';
import { IFlagService } from '#/app/flag/flag';
import { IBlobStore } from '#/persistence/interface/blobStore';
import { IAgentLifecycleService, MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
Expand All @@ -40,7 +39,6 @@ import {
fileHistoryKey,
} from './fileHistoryOps';
import { touchFileHistorySession } from './fileHistoryRetention';
import { FILE_HISTORY_FLAG_ID } from './flag';

export const FILE_HISTORY_MAX_FILE_BYTES = 4 * 1024 * 1024;
export { FILE_HISTORY_BLOB_PREFIX } from './fileHistory';
Expand All @@ -58,7 +56,6 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor
@IAgentToolExecutorService toolExecutor: IAgentToolExecutorService,
@IEventBus eventBus: IEventBus,
@IEventDispatcher private readonly dispatcher: IEventDispatcher,
@IFlagService private readonly flags: IFlagService,
@IAgentRuntimeService private readonly runtime: IAgentRuntimeService,
@IBlobStore private readonly blobs: IBlobStore,
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
Expand Down Expand Up @@ -89,17 +86,12 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor
eventBus.subscribe(TurnEnded, (event) => {
if (event.agentId !== this.agentCtx.agentId) return;
if (this.activeTurnId === event.turnId) this.activeTurnId = undefined;
if (!this.enabled()) return;
void this.enqueue(() => this.endCheckpoint(event.turnId));
}),
);
this.effect(() => () => this.queue, 'fileHistory:drain');
}

enabled(): boolean {
return this.flags.enabled(FILE_HISTORY_FLAG_ID);
}

history(): FileHistoryState {
return this.agentState.get(fileHistoryKey);
}
Expand All @@ -109,12 +101,10 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor
}

changes(turnId: number): Promise<FileHistoryChange[]> {
if (!this.enabled()) return Promise.resolve([]);
return this.enqueueValue(() => this.readChanges(turnId));
}

turnRecorded(turnId: number): Promise<boolean> {
if (!this.enabled()) return Promise.resolve(false);
return this.enqueueValue(async () => {
Comment thread
wbxl2000 marked this conversation as resolved.
const state = this.history();
const index = state.checkpoints.findIndex(
Expand All @@ -129,10 +119,17 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor
index === state.checkpoints.length - 1 &&
this.activeTurnId === turnId;
if (end === undefined && !live) return false;
const entries = { ...state.checkpoints[index]!.entries, ...end?.entries };
const keyed = Object.values(entries).find((entry) => entry.key !== null);
if (keyed?.key === null || keyed?.key === undefined) return true;
return this.blobs.has(this.agentCtx.scope(), keyed.key);
const keys = new Set<string>();
for (const entry of [
...Object.values(state.checkpoints[index]!.entries),
...Object.values(end?.entries ?? {}),
]) {
if (entry.key !== null) keys.add(entry.key);
}
for (const key of keys) {
if (!(await this.blobs.has(this.agentCtx.scope(), key))) return false;
}
return true;
});
}

Expand Down Expand Up @@ -219,7 +216,6 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor
path: string,
phase: FileHistoryCheckpointPhase = 'start',
): Promise<FileHistoryContent | undefined> {
if (!this.enabled()) return Promise.resolve(undefined);
return this.enqueueValue(() => this.readContentAt(turnId, path, phase));
}

Expand Down Expand Up @@ -255,14 +251,12 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor
}

private onWillExecuteTool(event: WillExecuteToolEvent): void {
if (!this.enabled()) return;
const path = editTargetPath(event.execution.display);
if (path === undefined) return;
event.waitUntil(this.enqueue(() => this.capture(path, event.turnId)));
Comment thread
wbxl2000 marked this conversation as resolved.
}

private onSubagentWillExecuteTool(event: WillExecuteToolEvent): void {
if (!this.enabled()) return;
const path = editTargetPath(event.execution.display);
if (path === undefined) return;
const main = this.agentLifecycle.handleOf(MAIN_AGENT_ID);
Expand All @@ -272,7 +266,7 @@ export class AgentFileHistoryService extends Service implements IAgentFileHistor

captureForActiveTurn(path: string): Promise<void> {
const turnId = this.activeTurnId;
if (!this.enabled() || turnId === undefined) return Promise.resolve();
if (turnId === undefined) return Promise.resolve();
return this.enqueue(() => this.capture(path, turnId));
}

Expand Down
16 changes: 0 additions & 16 deletions packages/agent-core-v2/src/features/fileHistory/flag.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ import '#/features/plan/planFeature';
export * from '#/features/fileHistory/fileHistory';
export * from '#/features/fileHistory/fileHistoryOps';
export * from '#/features/fileHistory/fileHistoryService';
export * from '#/features/fileHistory/flag';
import '#/features/fileHistory/fileHistoryFeature';
export * from '#/features/externalHooks/configSection';
export * from '#/features/externalHooks/app/externalHooksRunner';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ export class BlobStoreService implements IBlobStore {
}

async has(scope: string, key: string): Promise<boolean> {
const keys = await this.storage.list(scope, key);
return keys.includes(key);
return (await this.storage.size(scope, key)) !== undefined;
}

async delete(scope: string, key: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ import { TurnEnded } from '#/agent/loop/turnOps';
import { USER_PROMPT_ORIGIN } from '#/agent/contextMemory/types';
import { IEventBus, type ISessionEventBus } from '#/app/event/eventBus';
import { EventBusService } from '#/app/event/eventBusService';
import type { IFlagService } from '#/app/flag/flag';
import { IAgentFileHistoryService } from '#/features/fileHistory/fileHistory';
import { AgentFileHistoryService, countLineDiff } from '#/features/fileHistory/fileHistoryService';
import { displacedCheckpoints } from '#/features/fileHistory/fileHistoryOps';
import type { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
import { FILE_HISTORY_FLAG_ENV } from '#/features/fileHistory/flag';
import type { ToolCall } from '#/kosong/contract/message';
import type { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime';
import type { IHostFileSystem } from '#/os/interface/hostFileSystem';
Expand All @@ -39,7 +37,7 @@ import type { RunnableToolExecution } from '#/tool/toolContract';
import { createFakeHostFs } from '../../tools/fixtures/fake-exec';
import { stubToolExecutorEvents, type ToolExecutorEventStubs } from '../../agent/toolExecutor/stubs';
import { registerTestAgentWire, registerTestEventDispatcher, testWireScope } from '../../wire/stubs';
import { createTestAgent } from '../../harness';
import { createTestAgent, homeDirServices } from '../../harness';

const SCOPE = 'wire';
const KEY = 'file-history-test';
Expand All @@ -56,7 +54,6 @@ describe('AgentFileHistoryService', () => {
let blobs: IBlobStore;
let scopeCtx: IAgentScopeContext;
let files: Map<string, Uint8Array>;
let flagEnabled: boolean;

beforeEach(() => {
disposables = new DisposableStore();
Expand All @@ -79,7 +76,6 @@ describe('AgentFileHistoryService', () => {
executorEvents = stubToolExecutorEvents();
blobs = new BlobStoreService(new InMemoryStorageService());
files = new Map();
flagEnabled = true;
});

afterEach(() => {
Expand Down Expand Up @@ -119,7 +115,6 @@ describe('AgentFileHistoryService', () => {
agentId === scopeCtx.agentId
? scopeCtx
: makeAgentScopeContext({ agentId, agentScope: testWireScope(SCOPE, KEY) });
const flags = { enabled: () => flagEnabled } as unknown as IFlagService;
const workspace = {
workDir: WORK_DIR,
additionalDirs: [],
Expand All @@ -138,7 +133,6 @@ describe('AgentFileHistoryService', () => {
executorEvents.executor,
eventBus,
ix.get(IEventDispatcher),
flags,
stubRuntime(),
blobs,
workspace,
Expand Down Expand Up @@ -312,19 +306,6 @@ describe('AgentFileHistoryService', () => {
]);
});

it('does nothing while the flag is off', async () => {
flagEnabled = false;
const service = createService();
setFile('/ws/a.txt', 'content\n');

startTurn(1);
await fireEdit(service, '/ws/a.txt', 1);
await service.settled();

const state = service.history();
expect(state.checkpoints).toEqual([]);
expect(state.tracked).toEqual([]);
});

it('excludes user edits between turns via the end-of-turn checkpoint', async () => {
const service = createService();
Expand All @@ -349,25 +330,6 @@ describe('AgentFileHistoryService', () => {
expect(await service.contentAt(2, 'a.txt')).toBeUndefined();
});

it('guards reads once the flag is turned off after data was recorded', async () => {
const service = createService();
setFile('/ws/a.txt', 'content\n');

startTurn(1);
await fireEdit(service, '/ws/a.txt', 1);
setFile('/ws/a.txt', 'changed\n');
endTurn(1);
startTurn(2);
await service.settled();
expect(await service.changes(1)).toEqual([
{ path: 'a.txt', status: 'modified', additions: 1, deletions: 1 },
]);
expect((await service.contentAt(1, 'a.txt'))?.content).toBe('content\n');

flagEnabled = false;
expect(await service.changes(1)).toEqual([]);
expect(await service.contentAt(1, 'a.txt')).toBeUndefined();
});

it('reports an over-budget modified file as oversize with no counts', async () => {
const service = createService();
Expand Down Expand Up @@ -472,6 +434,28 @@ describe('AgentFileHistoryService', () => {
expect(await service.turnRecorded(3)).toBe(false);
});

it('reports a deletion turn unrecorded once its baseline blob is gone', async () => {
const service = createService();
setFile('/ws/d.txt', 'gone\n');

startTurn(1);
await fireEdit(service, '/ws/d.txt', 1);
files.delete('/ws/d.txt');
endTurn(1);
await service.settled();

expect(await service.changes(1)).toEqual([
{ path: 'd.txt', status: 'deleted', additions: 0, deletions: 1 },
]);
expect(await service.turnRecorded(1)).toBe(true);

const keyed = Object.values(
service.history().checkpoints.find((c) => c.turnId === 1 && c.phase !== 'end')!.entries,
).find((entry) => entry.key !== null);
await blobs.delete(scopeCtx.scope(), keyed!.key!);
expect(await service.turnRecorded(1)).toBe(false);
});

it('keeps a shared baseline blob alive until its last window reference leaves', async () => {
const service = createService();
setFile('/ws/s.txt', 'base\n');
Expand Down Expand Up @@ -522,19 +506,12 @@ describe('AgentFileHistoryService', () => {
});

describe('file history through real scripted turns', () => {
beforeEach(() => {
process.env[FILE_HISTORY_FLAG_ENV] = '1';
});

afterEach(() => {
delete process.env[FILE_HISTORY_FLAG_ENV];
});

it('checkpoints edits across turns and serves exact per-turn changes', async () => {
const dir = await mkdtemp(join(tmpdir(), 'file-history-e2e-'));
const home = await mkdtemp(join(tmpdir(), 'file-history-home-'));
const file = join(dir, 'notes.txt');
await writeFile(file, 'alpha\nbeta\n');
const ctx = createTestAgent();
const ctx = createTestAgent(homeDirServices(home));
try {
await ctx.rpc.setPermission({ mode: 'yolo' });

Expand Down Expand Up @@ -582,9 +559,14 @@ describe('file history through real scripted turns', () => {
{ path: file, status: 'modified', additions: 1, deletions: 1 },
]);
expect(await service.changes(1)).toEqual([]);

expect(await service.turnRecorded(0)).toBe(true);
expect(await service.turnRecorded(1)).toBe(false);
expect(await service.turnRecorded(99)).toBe(false);
} finally {
await ctx.dispose();
await rm(dir, { recursive: true, force: true });
await rm(home, { recursive: true, force: true });
}
});

Expand Down
1 change: 0 additions & 1 deletion packages/kap-server/src/protocol/rest-file-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export type WireFileHistoryChange = z.infer<typeof fileHistoryChangeSchema>;

export const fileHistoryChangesResponseSchema = z.object({
changes: z.array(fileHistoryChangeSchema),
enabled: z.boolean(),
recorded: z.boolean(),
});
export type FileHistoryChangesResponse = z.infer<typeof fileHistoryChangesResponseSchema>;
Expand Down
1 change: 0 additions & 1 deletion packages/kap-server/src/routes/fileHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function registerFileHistoryRoutes(app: FileHistoryRouteHost, core: Scope
okEnvelope(
{
changes: await history.changes(req.query.turn_id),
enabled: history.enabled(),
recorded: await history.turnRecorded(req.query.turn_id),
},
req.id,
Expand Down
1 change: 0 additions & 1 deletion packages/kap-server/test/fileHistory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ describe('file history routes', () => {
expect(changes.statusCode).toBe(200);
expect((changes.json() as Envelope<{ changes: unknown[] }>).data).toEqual({
changes: [],
enabled: false,
recorded: false,
});

Expand Down
Loading