Skip to content
Open
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/fix-session-stray-file-startup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix `kimi -c` failing to start with a storage error when a stray file is present in the session directory.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ const WATCH_DEBOUNCE_MS = 150;
const TORN_READ_RETRIES = 3;
const TORN_READ_RETRY_DELAY_MS = 15;

function isEnoent(error: unknown): boolean {
return (error as NodeJS.ErrnoException).code === 'ENOENT';
function isMissing(error: unknown): boolean {
const code = (error as NodeJS.ErrnoException).code;
return code === 'ENOENT' || code === 'ENOTDIR';
}

export class FileStorageService implements IFileSystemStorageService {
Expand All @@ -42,7 +43,7 @@ export class FileStorageService implements IFileSystemStorageService {
try {
bytes = await readFile(filePath);
} catch (error) {
if (isEnoent(error)) return undefined;
if (isMissing(error)) return undefined;
throw toStorageIoError(error, { path: filePath, op: 'read' });
}
if (attempt >= TORN_READ_RETRIES) return bytes;
Expand Down Expand Up @@ -72,7 +73,7 @@ export class FileStorageService implements IFileSystemStorageService {
yield chunk as Uint8Array;
}
} catch (error) {
if (isEnoent(error)) return;
if (isMissing(error)) return;
throw toStorageIoError(error, { path: filePath, op: 'read' });
}
}
Expand Down Expand Up @@ -144,7 +145,7 @@ export class FileStorageService implements IFileSystemStorageService {
try {
entries = await readdir(this.scopePath(scope));
} catch (error) {
if (isEnoent(error)) return [];
if (isMissing(error)) return [];
throw toStorageIoError(error, { path: this.scopePath(scope), op: 'list' });
}
return prefix === undefined ? entries : entries.filter((entry) => entry.startsWith(prefix));
Expand All @@ -155,7 +156,7 @@ export class FileStorageService implements IFileSystemStorageService {
try {
await unlink(filePath);
} catch (error) {
if (isEnoent(error)) return;
if (isMissing(error)) return;
throw toStorageIoError(error, { path: filePath, op: 'delete' });
}
}
Expand All @@ -165,7 +166,7 @@ export class FileStorageService implements IFileSystemStorageService {
try {
return (await stat(filePath)).size;
} catch (error) {
if (isEnoent(error)) return undefined;
if (isMissing(error)) return undefined;
throw toStorageIoError(error, { path: filePath, op: 'stat' });
}
}
Expand All @@ -175,7 +176,7 @@ export class FileStorageService implements IFileSystemStorageService {
try {
return (await stat(filePath)).mtimeMs;
} catch (error) {
if (isEnoent(error)) return undefined;
if (isMissing(error)) return undefined;
throw toStorageIoError(error, { path: filePath, op: 'stat' });
}
}
Expand Down Expand Up @@ -263,7 +264,7 @@ export class FileStorageService implements IFileSystemStorageService {
await syncDir(dir);
this.syncedDirs.add(dir);
} catch (error) {
if (!isEnoent(error)) throw error;
if (!isMissing(error)) throw error;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const REASONS: Record<StorageIoErrorCode, string> = {
function mapErrno(errno: string | undefined): StorageIoErrorCode {
switch (errno) {
case 'ENOENT':
case 'ENOTDIR':
return StorageErrors.codes.STORAGE_NOT_FOUND;
case 'EACCES':
case 'EPERM':
Expand Down
16 changes: 16 additions & 0 deletions packages/agent-core-v2/test/app/sessionIndex/sessionIndex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,22 @@ describe('FileSessionIndex (read model)', () => {
expect(await store.count({ workspaceIds: [workspaceId] })).toBe(2);
});

it('ignores non-directory junk entries (e.g. .DS_Store) in a workspace directory', async () => {
await seedSession('a', { title: 'a', createdAt: 1, updatedAt: 2 });
await fsp.writeFile(join(sessionsDir, workspaceId, '.DS_Store'), 'junk');

const store = build();
const status = await store.prepare();
expect(status).toEqual({ state: 'ready', generation: 1, degradedCount: 0 });

const page = await store.listRecent({ workspaceIds: [workspaceId] });
expect(page.items.map((s) => s.id)).toEqual(['a']);
expect(await store.count({ workspaceIds: [workspaceId] })).toBe(1);

await store.reconcileNow();
expect(store.status().state).toBe('ready');
});

it('the first read kicks one initial projection and shares its authoritative scan', async () => {
await seedSession('a', { title: 'a', createdAt: 1, updatedAt: 3 });
await seedSession('b', { title: 'b', createdAt: 2, updatedAt: 2 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { tmpdir } from 'node:os';
import { join } from 'pathe';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { toStorageIoError } from '#/persistence/interface/storage';

import { FileStorageService } from '#/persistence/backends/node-fs/fileStorageService';

const isWin = process.platform === 'win32';
Expand Down Expand Up @@ -86,6 +88,29 @@ describe('FileStorageService — error translation', () => {
details: { op: 'write', errno: expect.any(String) },
});
});

it('maps ENOTDIR to storage.not_found instead of io_failed', () => {
const error = toStorageIoError(
Object.assign(new Error('not a directory'), { code: 'ENOTDIR' }),
{ path: join(dir, 'scope', 'entry', 'state.json'), op: 'stat' },
);
expect(error.code).toBe('storage.not_found');
expect(error.details).toMatchObject({
path: join(dir, 'scope', 'entry', 'state.json'),
op: 'stat',
errno: 'ENOTDIR',
});
});

it('treats a path through a regular file as missing for read/mtime/size', async () => {
const svc = new FileStorageService(dir);
await mkdir(join(dir, 'scope'), { recursive: true });
await writeFile(join(dir, 'scope', '.DS_Store'), 'junk');

expect(await svc.read('scope/.DS_Store', 'state.json')).toBeUndefined();
expect(await svc.mtime('scope/.DS_Store', 'state.json')).toBeUndefined();
expect(await svc.size('scope/.DS_Store', 'state.json')).toBeUndefined();
});
});

describe('FileStorageService — writeStream', () => {
Expand Down