From a86d78cd8b4b1c316309c579ce233d0f123e5db6 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 23 Jun 2026 11:13:43 +0800 Subject: [PATCH] fix(acp): read workspace local config via local fs at session start Read `.kimi-code/local.toml` through the persistence (local) kaos instead of the ACP tool kaos during session bootstrap. The ACP reverse-RPC bridge needs the session registered on the client, which is not true until `session/new` returns, so reading through it failed with "unknown session" and blocked new threads in ACP editors such as Zed. Fixes #988 --- .changeset/fix-acp-new-thread-start.md | 5 ++ packages/agent-core/src/rpc/core-impl.ts | 16 ++++- .../agent-core/test/harness/runtime.test.ts | 71 ++++++++++++++++++- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-acp-new-thread-start.md diff --git a/.changeset/fix-acp-new-thread-start.md b/.changeset/fix-acp-new-thread-start.md new file mode 100644 index 0000000000..4cddd9d4ff --- /dev/null +++ b/.changeset/fix-acp-new-thread-start.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix ACP editors such as Zed failing to start a new thread. diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index e9a242f03a..54e07fd24d 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -228,7 +228,14 @@ export class KimiCore implements PromisableMethods { const withCallerMcp = mergeCallerMcpServers(baseMcpConfig, options.mcpServers); const parentKaos = overrides.kaos ?? (await this.getKaos()); const persistenceKaos = overrides.persistenceKaos ?? parentKaos; - const localWorkspaceDirs = await readWorkspaceAdditionalDirs(parentKaos, workDir); + // Read the workspace local config (`.kimi-code/local.toml`) through the + // persistence (local) kaos, not the tool kaos. In ACP mode the tool kaos is + // the reverse-RPC bridge and the client does not know the session yet during + // `session/new`, so reading through it fails with "unknown session" + // (https://github.com/MoonshotAI/kimi-code/issues/988). The local config is + // a system file and must not depend on the tool bridge — same reason + // `Session.systemContextKaos` is backed by the persistence sink. + const localWorkspaceDirs = await readWorkspaceAdditionalDirs(persistenceKaos, workDir); const callerAdditionalDirs = await resolveWorkspaceAdditionalDirs( parentKaos, workDir, @@ -351,7 +358,12 @@ export class KimiCore implements PromisableMethods { ): Promise { const summary = await this.sessionStore.get(input.sessionId); const parentKaosForRead = overrides.kaos ?? (await this.getKaos()); - const localWorkspaceDirs = await readWorkspaceAdditionalDirs(parentKaosForRead, summary.workDir); + // Read `.kimi-code/local.toml` through the persistence (local) kaos, not the + // tool kaos — see createSessionWithOverrides and issue #988. + const localWorkspaceDirs = await readWorkspaceAdditionalDirs( + overrides.persistenceKaos ?? parentKaosForRead, + summary.workDir, + ); const callerAdditionalDirs = await resolveWorkspaceAdditionalDirs( parentKaosForRead, summary.workDir, diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index c1136b099c..d9853289b6 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -1,6 +1,6 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { join } from 'pathe'; +import { join, normalize } from 'pathe'; import type { Kaos } from '@moonshot-ai/kaos'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -52,6 +52,36 @@ function rejectedKaos(error: Error): Promise { return promise; } +// Builds a Kaos that behaves like the ACP reverse-RPC bridge during +// `session/new`: reading a `local.toml` rejects with a non-ENOENT error because +// the client does not know the session yet (issue #988). Everything else +// delegates to the underlying kaos, so once the system-file read is routed +// through a working (local) kaos, session bootstrap can still proceed. +function createLocalTomlFailingKaos(base: Kaos): Kaos { + return new Proxy(base, { + get(target, prop, receiver) { + if (prop === 'readText') { + return ( + path: string, + options?: { encoding?: BufferEncoding; errors?: 'strict' | 'replace' | 'ignore' }, + ) => { + if (String(path).endsWith('local.toml')) { + return Promise.reject( + new Error(`acp: readTextFile failed for ${path}: unknown session (issue #988)`), + ); + } + return target.readText(path, options); + }; + } + if (prop === 'withCwd') { + return (cwd: string) => createLocalTomlFailingKaos(target.withCwd(cwd)); + } + const value = Reflect.get(target, prop, receiver); + return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(target) : value; + }, + }); +} + describe('KimiCore runtime config', () => { let tmp: string; @@ -190,6 +220,45 @@ micro_compaction = false expect(reloadedMainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true); }); + // Regression for https://github.com/MoonshotAI/kimi-code/issues/988: during + // ACP `session/new` the tool kaos is the reverse-RPC bridge and the client + // does not know the session yet, so reading `.kimi-code/local.toml` through + // it rejects. The workspace local config is a local system file and must be + // read through the persistence (local) kaos instead. + it('reads workspace local.toml through persistenceKaos during createSession', async () => { + tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); + const homeDir = join(tmp, 'home'); + const workDir = join(tmp, 'work'); + const sharedDir = join(tmp, 'shared'); + await mkdir(homeDir, { recursive: true }); + await mkdir(join(workDir, '.git'), { recursive: true }); + await mkdir(join(workDir, '.kimi-code'), { recursive: true }); + await mkdir(sharedDir, { recursive: true }); + await writeFile( + join(workDir, '.kimi-code', 'local.toml'), + `[workspace]\nadditional_dir = ["../shared"]\n`, + ); + await writeFile(join(homeDir, 'config.toml'), baseModelConfig()); + + const [coreRpc, sdkRpc] = createRPC(); + const core = new KimiCore(coreRpc, { homeDir }); + await sdkRpc({ + emitEvent: vi.fn(), + requestApproval: vi.fn(async (): Promise => ({ decision: 'rejected' })), + requestQuestion: vi.fn(async () => null), + toolCall: vi.fn(async () => ({ output: '' })), + }); + + const created = await core.createSessionWithOverrides( + { id: 'ses_runtime_local_toml_bootstrap', workDir, model: 'default-mock' }, + { kaos: createLocalTomlFailingKaos(testKaos), persistenceKaos: testKaos }, + ); + + const session = core.sessions.get(created.id); + expect(session).toBeDefined(); + expect(session?.getAdditionalDirs()).toContain(normalize(sharedDir)); + }); + it('uses the shared OAuth resolver for Moonshot service tokens', async () => { tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); const homeDir = join(tmp, 'home');