diff --git a/.changeset/fix-session-stray-file-startup.md b/.changeset/fix-session-stray-file-startup.md new file mode 100644 index 00000000000..66c91bf04e9 --- /dev/null +++ b/.changeset/fix-session-stray-file-startup.md @@ -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. diff --git a/packages/agent-core-v2/src/persistence/backends/node-fs/fileStorageService.ts b/packages/agent-core-v2/src/persistence/backends/node-fs/fileStorageService.ts index 7104423e6ca..5ed544d7eab 100644 --- a/packages/agent-core-v2/src/persistence/backends/node-fs/fileStorageService.ts +++ b/packages/agent-core-v2/src/persistence/backends/node-fs/fileStorageService.ts @@ -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 { @@ -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; @@ -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' }); } } @@ -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)); @@ -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' }); } } @@ -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' }); } } @@ -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' }); } } @@ -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; } } } diff --git a/packages/agent-core-v2/src/persistence/interface/storage.ts b/packages/agent-core-v2/src/persistence/interface/storage.ts index 0449c399f31..3a26c3f2ffd 100644 --- a/packages/agent-core-v2/src/persistence/interface/storage.ts +++ b/packages/agent-core-v2/src/persistence/interface/storage.ts @@ -95,6 +95,7 @@ const REASONS: Record = { function mapErrno(errno: string | undefined): StorageIoErrorCode { switch (errno) { case 'ENOENT': + case 'ENOTDIR': return StorageErrors.codes.STORAGE_NOT_FOUND; case 'EACCES': case 'EPERM': diff --git a/packages/agent-core-v2/test/app/sessionIndex/sessionIndex.test.ts b/packages/agent-core-v2/test/app/sessionIndex/sessionIndex.test.ts index 63146d8ec57..3c2a9657edd 100644 --- a/packages/agent-core-v2/test/app/sessionIndex/sessionIndex.test.ts +++ b/packages/agent-core-v2/test/app/sessionIndex/sessionIndex.test.ts @@ -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 }); diff --git a/packages/agent-core-v2/test/persistence/backends/node-fs/fileStorageService.test.ts b/packages/agent-core-v2/test/persistence/backends/node-fs/fileStorageService.test.ts index e8f140c4a2a..12b20f49a2c 100644 --- a/packages/agent-core-v2/test/persistence/backends/node-fs/fileStorageService.test.ts +++ b/packages/agent-core-v2/test/persistence/backends/node-fs/fileStorageService.test.ts @@ -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'; @@ -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', () => {