From 5871073bd8a62262946581e0007dd71abf5a5352 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 9 Sep 2026 21:59:34 +0800 Subject: [PATCH 1/2] chore: add fine-grained startup trace points across app, sdk, and engine --- apps/kimi-code/src/utils/startup-trace.ts | 3 +-- .../src/_base/utils/startupTrace.ts | 21 +++++++++++++++++++ .../workspaceInstanceManagerService.ts | 17 ++++++++++----- packages/node-sdk/src/sdk-rpc-client-v2.ts | 9 ++++++-- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 packages/agent-core-v2/src/_base/utils/startupTrace.ts diff --git a/apps/kimi-code/src/utils/startup-trace.ts b/apps/kimi-code/src/utils/startup-trace.ts index 65ac7eb441e..c2993f0533d 100644 --- a/apps/kimi-code/src/utils/startup-trace.ts +++ b/apps/kimi-code/src/utils/startup-trace.ts @@ -12,7 +12,6 @@ import path from 'node:path'; const enabled = process.env['KIMI_STARTUP_TRACE'] !== undefined && process.env['KIMI_STARTUP_TRACE'] !== ''; const logPath = process.env['KIMI_STARTUP_TRACE_LOG'] ?? '/tmp/kimi-startup-trace.log'; -const t0 = performance.now(); let prepared = false; export function startupTrace(label: string): void { @@ -27,7 +26,7 @@ export function startupTrace(label: string): void { } } try { - appendFileSync(logPath, `${(performance.now() - t0).toFixed(0).padStart(7)}ms ${label}\n`); + appendFileSync(logPath, `${performance.now().toFixed(0).padStart(7)}ms ${label}\n`); } catch { /* best effort */ } diff --git a/packages/agent-core-v2/src/_base/utils/startupTrace.ts b/packages/agent-core-v2/src/_base/utils/startupTrace.ts new file mode 100644 index 00000000000..47501d14fe6 --- /dev/null +++ b/packages/agent-core-v2/src/_base/utils/startupTrace.ts @@ -0,0 +1,21 @@ +import { appendFileSync, mkdirSync } from 'node:fs'; +import path from 'node:path'; + +const enabled = + process.env['KIMI_STARTUP_TRACE'] !== undefined && process.env['KIMI_STARTUP_TRACE'] !== ''; +const logPath = process.env['KIMI_STARTUP_TRACE_LOG'] ?? '/tmp/kimi-startup-trace.log'; +let prepared = false; + +export function startupTrace(label: string): void { + if (!enabled) return; + if (!prepared) { + prepared = true; + try { + mkdirSync(path.dirname(logPath), { recursive: true }); + appendFileSync(logPath, `--- ${new Date().toISOString()} pid=${process.pid} ---\n`); + } catch {} + } + try { + appendFileSync(logPath, `${performance.now().toFixed(0).padStart(7)}ms ${label}\n`); + } catch {} +} diff --git a/packages/agent-core-v2/src/workspace/workspaceInstance/workspaceInstanceManagerService.ts b/packages/agent-core-v2/src/workspace/workspaceInstance/workspaceInstanceManagerService.ts index 4537995107b..1e82bee9542 100644 --- a/packages/agent-core-v2/src/workspace/workspaceInstance/workspaceInstanceManagerService.ts +++ b/packages/agent-core-v2/src/workspace/workspaceInstance/workspaceInstanceManagerService.ts @@ -2,6 +2,7 @@ import { IInstantiationService, ref, type LiveRef } from '#/_base/di/instantiati import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Emitter } from '#/_base/event'; import { ILogService } from '#/_base/log/log'; +import { startupTrace } from '#/_base/utils/startupTrace'; import { IAgentIdentity } from '#/app/agentIdentity/agentIdentity'; import { IBuiltinAgentProfileLoader } from '#/app/agentProfileCatalog/builtinAgentProfileLoader'; import { IAgentProfileRegistry } from '#/app/agentProfileCatalog/agentProfileRegistry'; @@ -120,12 +121,14 @@ export class WorkspaceInstanceManager implements IWorkspaceInstanceManager { if (request !== undefined) return request; const promise = (async () => { let workspace: Workspace | undefined; + startupTrace('workspace:catalog:begin'); if ('workspaceId' in ref) { workspace = await this.workspaces.get(ref.workspaceId); if (workspace === undefined && ref.root !== undefined) workspace = await this.workspaces.createOrTouch(ref.root); } else { workspace = await this.workspaces.createOrTouch(ref.root); } + startupTrace('workspace:catalog:end'); if (workspace === undefined) throw new Error2(ErrorCodes.WORKSPACE_NOT_FOUND, `workspace ${'workspaceId' in ref ? ref.workspaceId : ref.root} does not exist`); const existing = this.instances.get(workspace.id); if (existing !== undefined) return existing; @@ -153,7 +156,7 @@ export class WorkspaceInstanceManager implements IWorkspaceInstanceManager { this.instances.delete(workspaceId); const attachments = this.attachments.get(workspaceId); this.attachments.delete(workspaceId); - if (attachments !== undefined) for (const attachment of [...attachments.values()].reverse()) await attachment.dispose(); + if (attachments !== undefined) for (const attachment of [...attachments.values()].toReversed()) await attachment.dispose(); await instance.dispose(); this.changeEmitter.fire({ workspaceId }); } @@ -169,23 +172,25 @@ export class WorkspaceInstanceManager implements IWorkspaceInstanceManager { } } catch (error) { this.providers.delete(factory.id); - for (const instance of attached.reverse()) await this.detach(instance.id, factory.id); + for (const instance of attached.toReversed()) await this.detach(instance.id, factory.id); throw error; } return { dispose: async () => { if (this.providers.get(factory.id) !== factory) return; this.providers.delete(factory.id); - for (const workspaceId of [...this.attachments.keys()].reverse()) await this.detach(workspaceId, factory.id); + for (const workspaceId of [...this.attachments.keys()].toReversed()) await this.detach(workspaceId, factory.id); } }; } async dispose(): Promise { - for (const workspaceId of [...this.instances.keys()].reverse()) await this.close(workspaceId); + for (const workspaceId of [...this.instances.keys()].toReversed()) await this.close(workspaceId); this.changeEmitter.dispose(); } private async materialize(workspace: Workspace): Promise { + startupTrace('workspace:materialize:begin'); await this.environment.ready; + startupTrace('workspace:materialize:envReady'); const runtimes = new RuntimeRegistry(workspace.id); const unitHost = this.unitHostFactory.create(this.instantiation, runtimes); const instance = new WorkspaceInstance( @@ -246,18 +251,20 @@ export class WorkspaceInstanceManager implements IWorkspaceInstanceManager { ), }, ); + startupTrace('workspace:materialize:constructed'); try { for (const provider of this.providers.values()) await this.attach(instance, provider); if (instance.runtimes.current('local') === undefined) throw new Error(`workspace ${workspace.id} has no local runtime`); instance.activate(); this.instances.set(workspace.id, instance); this.changeEmitter.fire({ workspaceId: workspace.id, instance }); + startupTrace('workspace:materialize:end'); return instance; } catch (error) { const attachments = this.attachments.get(instance.id); this.attachments.delete(instance.id); if (attachments !== undefined) { - for (const attachment of [...attachments.values()].reverse()) await attachment.dispose(); + for (const attachment of [...attachments.values()].toReversed()) await attachment.dispose(); } await instance.dispose(); throw error; diff --git a/packages/node-sdk/src/sdk-rpc-client-v2.ts b/packages/node-sdk/src/sdk-rpc-client-v2.ts index 734be22ec35..d761c91d8a6 100644 --- a/packages/node-sdk/src/sdk-rpc-client-v2.ts +++ b/packages/node-sdk/src/sdk-rpc-client-v2.ts @@ -131,6 +131,7 @@ import { readdir } from 'node:fs/promises'; import { join } from 'node:path'; import { encodeWorkDirKey } from '@moonshot-ai/agent-core-v2/_base/utils/workdir-slug'; +import { startupTrace } from '@moonshot-ai/agent-core-v2/_base/utils/startupTrace'; import { McpConnectionManager } from '@moonshot-ai/agent-core-v2/mcpCore/connection-manager'; import { loadMcpServers, @@ -688,10 +689,14 @@ export class SDKRpcClientV2 extends SDKRpcClientBase { * empty list rather than failing the caller. */ override async getWorkspaceTrustInfo(workDir: string): Promise { + startupTrace('workspaceTrust:getOrCreate:begin'); const handler = await this.engineAccessor .get(IWorkspaceInstanceManager) .getOrCreate({ root: workDir }); + startupTrace('workspaceTrust:getOrCreate:end'); + startupTrace('workspaceTrust:read:begin'); const trusted = await handler.program.trust.get(); + startupTrace('workspaceTrust:read:end'); if (trusted) return { trusted: true, gatedMcpServers: [] }; try { const fs = this.engineAccessor.get(IHostFileSystem); @@ -955,9 +960,9 @@ export class SDKRpcClientV2 extends SDKRpcClientBase { * cannot deadlock. */ private runSessionAccessAll(sessionIds: readonly string[], work: () => Promise): Promise { - const keys = [...new Set(sessionIds)].sort(); + const keys = [...new Set(sessionIds)].toSorted(); let chained: () => Promise = work; - for (const key of [...keys].reverse()) { + for (const key of [...keys].toReversed()) { const inner = chained; chained = () => this.runSessionAccess(key, inner); } From 5c2639a9f6e70509202d3880fad0a3e90fe8751f Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 9 Sep 2026 21:59:47 +0800 Subject: [PATCH 2/2] fix(agent-core-v2): disable skill-root directory watches by default The skill-root watches added in #3608 register recursive fs.watch roots on the entire OS home directory and the whole project root. On Windows these run through the native recursive leg, which delivers every subtree event to the main thread with no OS-level filtering, and each event costs an xstate transition (#3502). This is the prime suspect behind the reported 0.42.0 startup and interactive lag on Windows. Gate both watch sites behind KIMI_CODE_SKILL_ROOT_WATCH (default off) until the watches are redesigned to cover only the skill candidate directories. Skill catalogs still load at startup; new or changed skills are picked up on restart. --- .changeset/tame-ducks-watch.md | 5 ++ .../features/skill/catalog/skillRootWatch.ts | 7 +++ .../skill/catalog/userFileSkillSource.ts | 3 +- .../skill/workspace/rootFileSkillSource.ts | 2 + .../skill/workspace/skillCatalog.test.ts | 61 ++++++++++++++++++- 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 .changeset/tame-ducks-watch.md create mode 100644 packages/agent-core-v2/src/features/skill/catalog/skillRootWatch.ts diff --git a/.changeset/tame-ducks-watch.md b/.changeset/tame-ducks-watch.md new file mode 100644 index 00000000000..1d8b58fad0f --- /dev/null +++ b/.changeset/tame-ducks-watch.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Disable live watching of skill directories; new or changed skills are picked up on restart. Set KIMI_CODE_SKILL_ROOT_WATCH=1 to re-enable live refresh. diff --git a/packages/agent-core-v2/src/features/skill/catalog/skillRootWatch.ts b/packages/agent-core-v2/src/features/skill/catalog/skillRootWatch.ts new file mode 100644 index 00000000000..b61e564cb05 --- /dev/null +++ b/packages/agent-core-v2/src/features/skill/catalog/skillRootWatch.ts @@ -0,0 +1,7 @@ +import { parseBooleanEnv } from '#/_base/utils/env'; + +export const SKILL_ROOT_WATCH_ENV = 'KIMI_CODE_SKILL_ROOT_WATCH'; + +export function skillRootWatchEnabled(): boolean { + return parseBooleanEnv(process.env[SKILL_ROOT_WATCH_ENV]) === true; +} diff --git a/packages/agent-core-v2/src/features/skill/catalog/userFileSkillSource.ts b/packages/agent-core-v2/src/features/skill/catalog/userFileSkillSource.ts index 361c5cd0865..c1f5d42fbdf 100644 --- a/packages/agent-core-v2/src/features/skill/catalog/userFileSkillSource.ts +++ b/packages/agent-core-v2/src/features/skill/catalog/userFileSkillSource.ts @@ -16,6 +16,7 @@ import { type MergeAllAvailableSkillsConfig, } from './configSection'; import { ISkillDiscovery } from './skillDiscovery'; +import { skillRootWatchEnabled } from './skillRootWatch'; import { userRoots } from './skillRoots'; import { SKILL_SOURCE_PRIORITY, type ISkillSource, type SkillContribution } from './skillSource'; @@ -50,7 +51,7 @@ export class UserFileSkillSource extends Disposable implements IUserFileSkillSou if (event.domain === MERGE_ALL_AVAILABLE_SKILLS_SECTION) this.onDidChangeEmitter.fire(); }), ); - if ((this.bootstrap.args.skillDirs?.length ?? 0) === 0) { + if ((this.bootstrap.args.skillDirs?.length ?? 0) === 0 && skillRootWatchEnabled()) { this.watchUserSkillRoots(); } } diff --git a/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts b/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts index 29975ade550..fbb75449b47 100644 --- a/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts +++ b/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts @@ -11,6 +11,7 @@ import { } from '#/features/skill/catalog/configSection'; import { ISkillDiscovery } from '#/features/skill/catalog/skillDiscovery'; import { projectRoots, projectSkillRootCandidates } from '#/features/skill/catalog/skillRoots'; +import { skillRootWatchEnabled } from '#/features/skill/catalog/skillRootWatch'; import { SKILL_SOURCE_PRIORITY, type ISkillSource, @@ -80,6 +81,7 @@ export class WorkspaceRootSkillSource extends Disposable implements IWorkspaceRo private async updateProjectSkillRootWatch( scannedDirectories: readonly string[], ): Promise { + if (!skillRootWatchEnabled()) return false; const { projectRoot, candidates } = await projectSkillRootCandidates(this.workspace.cwd); const signature = [...scannedDirectories].toSorted().join('\0'); if (signature === this.watchSignature) return false; diff --git a/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts b/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts index dd72e8b8e90..244f14cf4fb 100644 --- a/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts +++ b/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts @@ -230,6 +230,8 @@ async function withSkillCatalogWorkspace( describe('WorkspaceSkillCatalogService', () => { beforeEach(() => { + vi.unstubAllEnvs(); + vi.stubEnv('KIMI_CODE_SKILL_ROOT_WATCH', '1'); watchMockState.calls = []; watchMockState.factory = undefined; _clearScopedRegistryForTests(); @@ -976,7 +978,7 @@ describe('WorkspaceSkillCatalogService', () => { await catalog.reloadSources(['user', 'explicit', 'extra', 'plugin']); sub.dispose(); - expect([...fired].sort()).toEqual(['explicit', 'extra', 'plugin', 'user']); + expect([...fired].toSorted()).toEqual(['explicit', 'extra', 'plugin', 'user']); expect(catalog.catalog.getSkill('user-skill')?.description).toBe('v2'); expect(catalog.catalog.getSkill('extra-skill')?.description).toBe('v2'); expect(catalog.catalog.getPluginSkill('demo', 'demo-skill')).toBeUndefined(); @@ -1089,6 +1091,63 @@ describe('WorkspaceSkillCatalogService', () => { } }); + it('does not watch the user skill roots when the watch env flag is off', async () => { + vi.stubEnv('KIMI_CODE_SKILL_ROOT_WATCH', '0'); + const host = createScopedTestHost([ + stubPair(IFlagService, stubFlag(true)), + stubPair(IBootstrapService, stubBootstrap('/home', {}, {}, '/os-home')), + stubPair(IConfigService, configStub()), + stubPair(IPluginService, pluginStub()), + stubPair(ILogService, stubLog()), + stubPair(ISkillDiscovery, new FileSkillDiscovery(stubLog())), + ]); + const workspace = host.child('program', 'w1', [ + stubPair(IWorkspaceContext, workspaceContextStub('/work')), + ]); + + try { + const catalog = workspace.accessor.get(IWorkspaceSkillCatalog); + await catalog.load(); + + const watchedPaths = watchMockState.calls.map((call) => call.path); + expect(watchedPaths).not.toContain('/home'); + expect(watchedPaths).not.toContain('/os-home'); + } finally { + host.dispose(); + } + }); + + it('does not watch the project skill root when the watch env flag is off', async () => { + vi.stubEnv('KIMI_CODE_SKILL_ROOT_WATCH', '0'); + const workDir = await mkdtemp(join(tmpdir(), 'skill-watch-disabled-')); + const skillRoot = join(workDir, '.agents', 'skills'); + await mkdir(skillRoot, { recursive: true }); + const watchedRoot = await realpath(workDir); + const host = createScopedTestHost([ + stubPair(IFlagService, stubFlag(true)), + stubPair(IBootstrapService, bootstrapStub), + stubPair(IConfigService, configStub()), + stubPair(IPluginService, pluginStub()), + stubPair(ILogService, stubLog()), + stubPair(ISkillDiscovery, new FileSkillDiscovery(stubLog())), + ]); + const workspace = host.child('program', 'w1', [ + stubPair(IWorkspaceContext, workspaceContextStub(workDir)), + ]); + + try { + const catalog = workspace.accessor.get(IWorkspaceSkillCatalog); + await catalog.load(); + + const watchedPaths = watchMockState.calls.map((call) => call.path); + expect(watchedPaths).not.toContain(workDir); + expect(watchedPaths).not.toContain(watchedRoot); + } finally { + host.dispose(); + await rm(workDir, { recursive: true, force: true }); + } + }); + it('disposes the user root watches when the app scope is disposed', async () => { const handles: { disposed: boolean }[] = []; watchMockState.factory = () => {