From 5871073bd8a62262946581e0007dd71abf5a5352 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 9 Sep 2026 21:59:34 +0800 Subject: [PATCH 1/6] 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/6] 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 = () => { From 8a2c79026bdc022f17d2d7714dc752a3b2d823ed Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 15 Sep 2026 15:36:44 +0800 Subject: [PATCH 3/6] fix(agent-core-v2): stop escalating query-store lock contention to a rebuild Lock contention (LockError, including cross-shard AggregateError) means another live process holds the shard locks, so closing the cluster and attempting a full wipe-and-rebuild can never succeed and only amplifies IO: it retries all 16 shard locks, reopens the cluster, and re-fans-out index definitions. Contention errors now throw straight through without counting toward the transient-failure escalation. To make contention observable in the field, a throttled (60s) warn logs the cluster stats (lockWaits, writerOpens, readerReopens, incrementalCatchups, catchupFramesApplied, evictions) on contention. --- .changeset/eager-pandas-lock.md | 5 ++ .../backends/minidb/miniDbQueryStore.ts | 32 +++++++- .../backends/minidb/miniDbQueryStore.test.ts | 75 +++++++++++++++++-- 3 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 .changeset/eager-pandas-lock.md diff --git a/.changeset/eager-pandas-lock.md b/.changeset/eager-pandas-lock.md new file mode 100644 index 00000000000..34a8d61dc33 --- /dev/null +++ b/.changeset/eager-pandas-lock.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix repeated session-index rebuild attempts when running multiple Kimi Code instances at the same time. diff --git a/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts b/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts index f6313548c27..ec19eb4f1d4 100644 --- a/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts +++ b/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts @@ -1,7 +1,7 @@ import { join } from 'pathe'; import { classifyStorageError, type QueryOptions } from '@moonshot-ai/minidb'; -import { ClusterDb, wipeCluster } from '@moonshot-ai/minidb/cluster'; +import { ClusterDb, LockError, wipeCluster } from '@moonshot-ai/minidb/cluster'; import { Disposable, toDisposable } from '#/_base/di/lifecycle'; import { LifecycleScope } from '#/app/scopes'; @@ -29,6 +29,7 @@ const SHARD_COUNT = 16; const LOCK_ACQUIRE_TIMEOUT_MS = 1000; const DROP_BATCH_SIZE = 500; const TRANSIENT_ESCALATION_LIMIT = 5; +const CONTENTION_LOG_INTERVAL_MS = 60_000; function physicalKey(collection: string, key: string): string { return `${collection}${SEP}${key}`; @@ -38,6 +39,11 @@ function indexName(collection: string, name: string): string { return `${collection}:${name}`; } +export function isLockContentionError(error: unknown): boolean { + if (error instanceof LockError) return true; + return error instanceof AggregateError && error.errors.some(isLockContentionError); +} + const pendingDisposals = new Set>(); export async function drainQueryStoreDisposals(): Promise { @@ -53,6 +59,7 @@ export class MiniDbQueryStore extends Disposable implements IQueryStore { private transientReadFailures = 0; private transientWriteFailures = 0; private storeEpochCounter = 0; + private lastContentionLogAt = 0; private readonly ensuredIndexes = new Set(); constructor( @@ -140,6 +147,10 @@ export class MiniDbQueryStore extends Disposable implements IQueryStore { return result; } catch (error) { if (classifyStorageError(error) !== 'rebuild') { + if (isLockContentionError(error)) { + this.noteLockContention(db, kind, error); + throw error; + } const failures = kind === 'write' ? (this.transientWriteFailures += 1) @@ -154,6 +165,25 @@ export class MiniDbQueryStore extends Disposable implements IQueryStore { } } + private noteLockContention(db: ClusterDb, kind: 'read' | 'write', error: unknown): void { + const now = Date.now(); + if (now - this.lastContentionLogAt < CONTENTION_LOG_INTERVAL_MS) return; + this.lastContentionLogAt = now; + const stats = db.stats(); + this.log.warn('minidb query-store lock contention: shard locks held by another process', { + dir: this.dir, + kind, + error: String(error), + lockWaits: stats.lockWaits, + writerOpens: stats.writerOpens, + readerOpens: stats.readerOpens, + readerReopens: stats.readerReopens, + incrementalCatchups: stats.incrementalCatchups, + catchupFramesApplied: stats.catchupFramesApplied, + evictions: stats.evictions, + }); + } + async put( collection: string, key: string, diff --git a/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts b/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts index 0a5bbcbd444..c7c4d8b0734 100644 --- a/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts +++ b/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { promises as fsp } from 'node:fs'; import os from 'node:os'; @@ -8,8 +8,13 @@ import { ScopeActivation, _clearScopedRegistryForTests, registerScopedService } import { createScopedTestHost, stubPair } from '#/_base/di/test'; import { ILogService } from '#/_base/log/log'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; -import { ClusterDb } from '@moonshot-ai/minidb/cluster'; -import { drainQueryStoreDisposals, MiniDbQueryStore } from '#/persistence/backends/minidb/miniDbQueryStore'; +import { MiniDb } from '@moonshot-ai/minidb'; +import { ClusterDb, LockError, shardDirName, shardFor } from '@moonshot-ai/minidb/cluster'; +import { + drainQueryStoreDisposals, + isLockContentionError, + MiniDbQueryStore, +} from '#/persistence/backends/minidb/miniDbQueryStore'; import { IQueryStore, QueryStoreRebuiltError } from '#/persistence/interface/queryStore'; import { stubBootstrap } from '../../../app/bootstrap/stubs'; import { stubLog } from '../../../_base/log/stubs'; @@ -40,10 +45,10 @@ describe('MiniDbQueryStore', () => { await fsp.rm(homeDir, { recursive: true, force: true }); }); - function build(): IQueryStore { + function build(log: ILogService = stubLog()): IQueryStore { const host = createScopedTestHost([ stubPair(IBootstrapService, stubBootstrap(homeDir)), - stubPair(ILogService, stubLog()), + stubPair(ILogService, log), ]); disposeHost = () => { host.dispose(); }; return host.app.accessor.get(IQueryStore); @@ -154,6 +159,64 @@ describe('MiniDbQueryStore', () => { } }); + it('lock contention from another process fails writes without escalating to a rebuild', async () => { + const warn = vi.fn(); + const logStub: ILogService = { + _serviceBrand: undefined, + level: 'info', + setLevel: () => {}, + flush: () => Promise.resolve(), + error: () => {}, + warn, + info: () => {}, + debug: () => {}, + child: () => logStub, + }; + const store = build(logStub); + const physicalKey = (key: string) => `${COLLECTION}${SEP}${key}`; + const storeDir = join(homeDir, 'cache', 'query-store'); + const lockedShardId = shardFor(physicalKey('locked'), 16); + const freeKeys: string[] = []; + for (let i = 0; freeKeys.length < 2; i++) { + const candidate = `free-${i}`; + if (shardFor(physicalKey(candidate), 16) !== lockedShardId) freeKeys.push(candidate); + } + await store.put(COLLECTION, freeKeys[0]!, { id: freeKeys[0], v: 0 }); + + const holder = await MiniDb.open({ + dir: join(storeDir, shardDirName(lockedShardId, 16)), + valueCodec: 'json', + }); + try { + const attemptsBeyondEscalation = 6; + for (let attempt = 0; attempt < attemptsBeyondEscalation; attempt++) { + await expect(store.put(COLLECTION, 'locked', { id: 'locked' })).rejects.toThrow(/locked/); + } + expect(warn.mock.calls.some(([message]) => String(message).includes('rebuilt'))).toBe(false); + const contentionLogs = warn.mock.calls.filter(([message]) => + String(message).includes('lock contention'), + ); + expect(contentionLogs).toHaveLength(1); + await store.put(COLLECTION, freeKeys[1]!, { id: freeKeys[1], v: 2 }); + expect(await store.get(COLLECTION, freeKeys[0]!)).toEqual({ id: freeKeys[0], v: 0 }); + } finally { + await holder.close(); + } + await store.put(COLLECTION, 'locked', { id: 'locked', v: 3 }); + expect(await store.get(COLLECTION, 'locked')).toEqual({ id: 'locked', v: 3 }); + }, 30_000); + + it('classifies lock errors, including cross-shard aggregate failures, as contention', () => { + expect(isLockContentionError(new LockError('locked'))).toBe(true); + expect( + isLockContentionError( + new AggregateError([new LockError('a'), new Error('io')], 'batch failed on 1/2 shard(s)'), + ), + ).toBe(true); + expect(isLockContentionError(new AggregateError([new Error('io')], 'batch failed'))).toBe(false); + expect(isLockContentionError(new Error('io'))).toBe(false); + }); + it('wipes and rebuilds the store after the cluster registry is corrupted', async () => { const first = build(); await first.put(COLLECTION, 'a', { id: 'a', v: 1 }); @@ -237,7 +300,7 @@ describe('MiniDbQueryStore', () => { { kind: 'put', collection: COLLECTION, key: 'b', value: { v: 2 } }, ]); const found = await store.getMany<{ v: number }>(COLLECTION, ['a', 'missing', 'b']); - expect([...found.keys()].sort()).toEqual(['a', 'b']); + expect([...found.keys()].toSorted()).toEqual(['a', 'b']); expect(found.get('a')).toEqual({ v: 1 }); expect(found.get('b')).toEqual({ v: 2 }); expect(await store.getMany(COLLECTION, [])).toEqual(new Map()); From 38237dad9fccc78e65a2bfaefec49f9f21bd609f Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 15 Sep 2026 15:39:07 +0800 Subject: [PATCH 4/6] chore: drop the contention test and changeset for the diagnostic build --- .changeset/eager-pandas-lock.md | 5 -- .../backends/minidb/miniDbQueryStore.test.ts | 73 ++----------------- 2 files changed, 5 insertions(+), 73 deletions(-) delete mode 100644 .changeset/eager-pandas-lock.md diff --git a/.changeset/eager-pandas-lock.md b/.changeset/eager-pandas-lock.md deleted file mode 100644 index 34a8d61dc33..00000000000 --- a/.changeset/eager-pandas-lock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Fix repeated session-index rebuild attempts when running multiple Kimi Code instances at the same time. diff --git a/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts b/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts index c7c4d8b0734..f2ae7e5a582 100644 --- a/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts +++ b/packages/agent-core-v2/test/persistence/backends/minidb/miniDbQueryStore.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { promises as fsp } from 'node:fs'; import os from 'node:os'; @@ -8,13 +8,8 @@ import { ScopeActivation, _clearScopedRegistryForTests, registerScopedService } import { createScopedTestHost, stubPair } from '#/_base/di/test'; import { ILogService } from '#/_base/log/log'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; -import { MiniDb } from '@moonshot-ai/minidb'; -import { ClusterDb, LockError, shardDirName, shardFor } from '@moonshot-ai/minidb/cluster'; -import { - drainQueryStoreDisposals, - isLockContentionError, - MiniDbQueryStore, -} from '#/persistence/backends/minidb/miniDbQueryStore'; +import { ClusterDb } from '@moonshot-ai/minidb/cluster'; +import { drainQueryStoreDisposals, MiniDbQueryStore } from '#/persistence/backends/minidb/miniDbQueryStore'; import { IQueryStore, QueryStoreRebuiltError } from '#/persistence/interface/queryStore'; import { stubBootstrap } from '../../../app/bootstrap/stubs'; import { stubLog } from '../../../_base/log/stubs'; @@ -45,10 +40,10 @@ describe('MiniDbQueryStore', () => { await fsp.rm(homeDir, { recursive: true, force: true }); }); - function build(log: ILogService = stubLog()): IQueryStore { + function build(): IQueryStore { const host = createScopedTestHost([ stubPair(IBootstrapService, stubBootstrap(homeDir)), - stubPair(ILogService, log), + stubPair(ILogService, stubLog()), ]); disposeHost = () => { host.dispose(); }; return host.app.accessor.get(IQueryStore); @@ -159,64 +154,6 @@ describe('MiniDbQueryStore', () => { } }); - it('lock contention from another process fails writes without escalating to a rebuild', async () => { - const warn = vi.fn(); - const logStub: ILogService = { - _serviceBrand: undefined, - level: 'info', - setLevel: () => {}, - flush: () => Promise.resolve(), - error: () => {}, - warn, - info: () => {}, - debug: () => {}, - child: () => logStub, - }; - const store = build(logStub); - const physicalKey = (key: string) => `${COLLECTION}${SEP}${key}`; - const storeDir = join(homeDir, 'cache', 'query-store'); - const lockedShardId = shardFor(physicalKey('locked'), 16); - const freeKeys: string[] = []; - for (let i = 0; freeKeys.length < 2; i++) { - const candidate = `free-${i}`; - if (shardFor(physicalKey(candidate), 16) !== lockedShardId) freeKeys.push(candidate); - } - await store.put(COLLECTION, freeKeys[0]!, { id: freeKeys[0], v: 0 }); - - const holder = await MiniDb.open({ - dir: join(storeDir, shardDirName(lockedShardId, 16)), - valueCodec: 'json', - }); - try { - const attemptsBeyondEscalation = 6; - for (let attempt = 0; attempt < attemptsBeyondEscalation; attempt++) { - await expect(store.put(COLLECTION, 'locked', { id: 'locked' })).rejects.toThrow(/locked/); - } - expect(warn.mock.calls.some(([message]) => String(message).includes('rebuilt'))).toBe(false); - const contentionLogs = warn.mock.calls.filter(([message]) => - String(message).includes('lock contention'), - ); - expect(contentionLogs).toHaveLength(1); - await store.put(COLLECTION, freeKeys[1]!, { id: freeKeys[1], v: 2 }); - expect(await store.get(COLLECTION, freeKeys[0]!)).toEqual({ id: freeKeys[0], v: 0 }); - } finally { - await holder.close(); - } - await store.put(COLLECTION, 'locked', { id: 'locked', v: 3 }); - expect(await store.get(COLLECTION, 'locked')).toEqual({ id: 'locked', v: 3 }); - }, 30_000); - - it('classifies lock errors, including cross-shard aggregate failures, as contention', () => { - expect(isLockContentionError(new LockError('locked'))).toBe(true); - expect( - isLockContentionError( - new AggregateError([new LockError('a'), new Error('io')], 'batch failed on 1/2 shard(s)'), - ), - ).toBe(true); - expect(isLockContentionError(new AggregateError([new Error('io')], 'batch failed'))).toBe(false); - expect(isLockContentionError(new Error('io'))).toBe(false); - }); - it('wipes and rebuilds the store after the cluster registry is corrupted', async () => { const first = build(); await first.put(COLLECTION, 'a', { id: 'a', v: 1 }); From 05d776e52d863b78ecf521611c13a76758da8ba7 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 15 Sep 2026 15:49:25 +0800 Subject: [PATCH 5/6] chore(agent-core-v2): add event-loop stall monitor and session-index slow-path timing logs The Windows lag report has three unproven suspects beyond shard lock contention: main-thread event-loop stalls, degraded session-index reads degenerating into full session-tree scans, and slow reconcile ticks. Add direct instrumentation for each: - EventLoopMonitorService (App scope, KIMI_CODE_EVENT_LOOP_MONITOR=1): per-second event-loop delay sampling, warn on stalls >= 250ms with p50/p99/max, plus a 60s summary line. - tick(): duration on the reconciliation-failed warn, plus a slow reconcile warn (>= 2s). - collectAuthoritative(): slow full-scan warn (>= 2s) with session count and index state. - mirror flush failure warn gains durationMs. --- .../src/app/diagnostics/eventLoopMonitor.ts | 67 +++++++++++++++++++ .../sessionIndex/sessionIndexMirrorService.ts | 2 + .../app/sessionIndex/sessionIndexService.ts | 41 ++++++++++-- packages/agent-core-v2/src/index.ts | 1 + 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts diff --git a/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts b/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts new file mode 100644 index 00000000000..f1c8d3af550 --- /dev/null +++ b/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts @@ -0,0 +1,67 @@ +import { monitorEventLoopDelay, type IntervalHistogram } from 'node:perf_hooks'; + +import { Disposable } from '#/_base/di/lifecycle'; +import { LifecycleScope } from '#/app/scopes'; +import { createDecorator } from '#/_base/di/instantiation'; +import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { ILogService } from '#/_base/log/log'; +import { IntervalTimer } from '#/_base/utils/timer'; +import { IBootstrapService } from '#/app/bootstrap/bootstrap'; + +const SAMPLE_INTERVAL_MS = 1_000; +const SUMMARY_EVERY_TICKS = 60; +const STALL_WARN_MS = 250; +const ENABLE_ENV = 'KIMI_CODE_EVENT_LOOP_MONITOR'; + +export const IEventLoopMonitorService = + createDecorator('eventLoopMonitor'); + +export class EventLoopMonitorService extends Disposable { + declare readonly _serviceBrand: undefined; + + private readonly timer = this._register(new IntervalTimer({ unref: true })); + private readonly histogram: IntervalHistogram | undefined; + private ticks = 0; + + constructor( + @IBootstrapService bootstrap: IBootstrapService, + @ILogService private readonly log: ILogService, + ) { + super(); + if (bootstrap.getEnv(ENABLE_ENV) === undefined) return; + this.histogram = monitorEventLoopDelay({ resolution: 20 }); + this.histogram.enable(); + this.timer.cancelAndSet(() => this.sample(), SAMPLE_INTERVAL_MS); + } + + private sample(): void { + const histogram = this.histogram; + if (histogram === undefined) return; + this.ticks += 1; + const maxMs = histogram.max / 1e6; + if (maxMs >= STALL_WARN_MS) { + this.log.warn('event loop stall detected', { + maxMs: Math.round(maxMs), + p99Ms: Math.round(histogram.percentile(99) / 1e6), + p50Ms: Math.round(histogram.percentile(50) / 1e6), + }); + } + if (this.ticks % SUMMARY_EVERY_TICKS === 0) { + this.log.info('event loop delay summary', { + p50Ms: Math.round(histogram.percentile(50) / 1e6), + p90Ms: Math.round(histogram.percentile(90) / 1e6), + p99Ms: Math.round(histogram.percentile(99) / 1e6), + maxMs: Math.round(histogram.max / 1e6), + }); + } + histogram.reset(); + } +} + +registerScopedService( + LifecycleScope.App, + IEventLoopMonitorService, + EventLoopMonitorService, + ScopeActivation.OnScopeCreated, + 'diagnostics', +); diff --git a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts index 61ac995c148..813dff01015 100644 --- a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts +++ b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts @@ -130,6 +130,7 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro private async flushChunk(): Promise { const chunk = [...this.pendingMap.entries()].slice(0, FLUSH_BATCH_SIZE); if (chunk.length === 0) return; + const startedAt = Date.now(); try { const manifest = await this.queryStore.getCheckpoint(SESSION_INDEX_MANIFEST); if (manifest === undefined) { @@ -197,6 +198,7 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro if (this.consecutiveFailures === 1) { this.log.warn('failed to flush session index mirror chunk', { pending: this.pendingMap.size, + durationMs: Date.now() - startedAt, error: String(error), }); } diff --git a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts index 45b2b330d48..081ce96dc41 100644 --- a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts +++ b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts @@ -54,6 +54,8 @@ import { const RECONCILE_INTERVAL_MS = 60_000; const DEGRADED_RETRY_MS = 5_000; const TIE_REPAIR_LIMIT = 1_000; +const SLOW_RECONCILE_MS = 2_000; +const SLOW_AUTHORITATIVE_SCAN_MS = 2_000; const UNBOUNDED = Number.MAX_SAFE_INTEGER; function canonicalOrder(a: SessionSummary, b: SessionSummary): number { @@ -259,6 +261,7 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { return; } if (this.state !== 'ready') return; + const startedAt = Date.now(); try { const manifest = await this.queryStore.getCheckpoint(SESSION_INDEX_MANIFEST); if (manifest === undefined) { @@ -269,8 +272,18 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { this.generation = manifest.seq; if (await this.manifestFresh(manifest)) return; await this.projector.reconcile(manifest.seq); + const durationMs = Date.now() - startedAt; + if (durationMs >= SLOW_RECONCILE_MS) { + this.log.warn('session index reconciliation slow', { + durationMs, + generation: manifest.seq, + }); + } } catch (error) { - this.log.warn('session index reconciliation failed', { error: String(error) }); + this.log.warn('session index reconciliation failed', { + error: String(error), + durationMs: Date.now() - startedAt, + }); } } @@ -656,6 +669,7 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { private async collectAuthoritative( workspaceIds: readonly string[] | undefined, ): Promise { + const startedAt = Date.now(); let collected: SessionSummary[]; if ( this.readModelEnabled() && @@ -677,13 +691,26 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { } } const pending = this.mirror.pending(); - if (pending.length === 0) return collected; - const byId = new Map(collected.map((summary) => [summary.id, summary])); - for (const summary of pending) { - if (workspaceIds !== undefined && !workspaceIds.includes(summary.workspaceId)) continue; - byId.set(summary.id, summary); + let result: SessionSummary[]; + if (pending.length === 0) { + result = collected; + } else { + const byId = new Map(collected.map((summary) => [summary.id, summary])); + for (const summary of pending) { + if (workspaceIds !== undefined && !workspaceIds.includes(summary.workspaceId)) continue; + byId.set(summary.id, summary); + } + result = [...byId.values()]; + } + const durationMs = Date.now() - startedAt; + if (durationMs >= SLOW_AUTHORITATIVE_SCAN_MS) { + this.log.warn('session index authoritative scan slow', { + durationMs, + sessions: result.length, + state: this.state, + }); } - return [...byId.values()]; + return result; } private readModelEnabled(): boolean { diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 688cd5e3226..1b339247330 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -168,6 +168,7 @@ export type { KimiThinkingConfig } from '#human/llm-kimi/trait'; export * from '#/app/sessionIndex/sessionIndex'; export * from '#/app/sessionIndex/sessionIndexService'; export * from '#/app/sessionIndex/sessionIndexMirrorService'; +export * from '#/app/diagnostics/eventLoopMonitor'; export * from '#/session/sessionMetadata/sessionMetadata'; export * from '#/session/sessionMetadata/sessionMetadataService'; export * from '#/session/sessionMetadata/promptMetadata'; From 936fec283a2193e413f4ee70d0364c775811be42 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 15 Sep 2026 16:07:42 +0800 Subject: [PATCH 6/6] chore(agent-core-v2): gate diagnostic logging and the contention fix behind env vars So the affected machine can run an elimination matrix: - KIMI_CODE_DIAG=1 enables the event-loop stall monitor, the slow reconcile / authoritative-scan warns, and the durationMs fields (default off; KIMI_CODE_EVENT_LOOP_MONITOR is folded into it). - KIMI_CODE_QUERY_STORE_REBUILD_ON_CONTENTION=1 restores the previous behavior where lock contention counts toward the transient-failure escalation and triggers wipe-and-rebuild attempts. Default is the fixed behavior (contention never escalates); the throttled contention stats warn logs in both modes so contention stays observable while toggling. --- packages/agent-core-v2/src/app/diagnostics/diag.ts | 12 ++++++++++++ .../src/app/diagnostics/eventLoopMonitor.ts | 11 ++++------- .../app/sessionIndex/sessionIndexMirrorService.ts | 4 +++- .../src/app/sessionIndex/sessionIndexService.ts | 8 +++++--- packages/agent-core-v2/src/index.ts | 1 + .../persistence/backends/minidb/miniDbQueryStore.ts | 5 ++++- 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 packages/agent-core-v2/src/app/diagnostics/diag.ts diff --git a/packages/agent-core-v2/src/app/diagnostics/diag.ts b/packages/agent-core-v2/src/app/diagnostics/diag.ts new file mode 100644 index 00000000000..488508eefc3 --- /dev/null +++ b/packages/agent-core-v2/src/app/diagnostics/diag.ts @@ -0,0 +1,12 @@ +import { parseBooleanEnv } from '#/_base/utils/env'; + +export const DIAG_ENV = 'KIMI_CODE_DIAG'; +export const REBUILD_ON_CONTENTION_ENV = 'KIMI_CODE_QUERY_STORE_REBUILD_ON_CONTENTION'; + +export function diagEnabled(): boolean { + return parseBooleanEnv(process.env[DIAG_ENV]) === true; +} + +export function rebuildOnContentionEnabled(): boolean { + return parseBooleanEnv(process.env[REBUILD_ON_CONTENTION_ENV]) === true; +} diff --git a/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts b/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts index f1c8d3af550..83369b712e8 100644 --- a/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts +++ b/packages/agent-core-v2/src/app/diagnostics/eventLoopMonitor.ts @@ -6,12 +6,12 @@ import { createDecorator } from '#/_base/di/instantiation'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { ILogService } from '#/_base/log/log'; import { IntervalTimer } from '#/_base/utils/timer'; -import { IBootstrapService } from '#/app/bootstrap/bootstrap'; + +import { diagEnabled } from './diag'; const SAMPLE_INTERVAL_MS = 1_000; const SUMMARY_EVERY_TICKS = 60; const STALL_WARN_MS = 250; -const ENABLE_ENV = 'KIMI_CODE_EVENT_LOOP_MONITOR'; export const IEventLoopMonitorService = createDecorator('eventLoopMonitor'); @@ -23,12 +23,9 @@ export class EventLoopMonitorService extends Disposable { private readonly histogram: IntervalHistogram | undefined; private ticks = 0; - constructor( - @IBootstrapService bootstrap: IBootstrapService, - @ILogService private readonly log: ILogService, - ) { + constructor(@ILogService private readonly log: ILogService) { super(); - if (bootstrap.getEnv(ENABLE_ENV) === undefined) return; + if (!diagEnabled()) return; this.histogram = monitorEventLoopDelay({ resolution: 20 }); this.histogram.enable(); this.timer.cancelAndSet(() => this.sample(), SAMPLE_INTERVAL_MS); diff --git a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts index 813dff01015..07ee75c9f70 100644 --- a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts +++ b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts @@ -12,6 +12,7 @@ import { IFileSystemStorageService } from '#/persistence/interface/storage'; import { ISessionIndexMirror, type SessionSummary } from './sessionIndex'; import { markSessionDirty } from './sessionIndexDirtyJournal'; +import { diagEnabled } from '#/app/diagnostics/diag'; import { SESSION_INDEX_MANIFEST, recencyColumn, @@ -41,6 +42,7 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro private flushing: Promise | undefined; private consecutiveFailures = 0; private giveUpTracked = false; + private readonly diag = diagEnabled(); private disposed = false; private overflowLogged = false; private readonly sessionsScope: string; @@ -198,7 +200,7 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro if (this.consecutiveFailures === 1) { this.log.warn('failed to flush session index mirror chunk', { pending: this.pendingMap.size, - durationMs: Date.now() - startedAt, + durationMs: this.diag ? Date.now() - startedAt : undefined, error: String(error), }); } diff --git a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts index 081ce96dc41..c0e7b9dd3b7 100644 --- a/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts +++ b/packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts @@ -32,6 +32,7 @@ import { type SessionSummary, } from './sessionIndex'; import { markSessionDirty } from './sessionIndexDirtyJournal'; +import { diagEnabled } from '#/app/diagnostics/diag'; import { PARENT_INDEX_NAME, SESSION_INDEX_MANIFEST, @@ -86,6 +87,7 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { private lastDegradedKey: string | undefined; private prepareFlight: Promise | undefined; private projectFlight: Promise | undefined; + private readonly diag = diagEnabled(); private readonly reconcileTimer = this._register(new IntervalTimer({ unref: true })); private readonly projector: SessionIndexProjector; @@ -273,7 +275,7 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { if (await this.manifestFresh(manifest)) return; await this.projector.reconcile(manifest.seq); const durationMs = Date.now() - startedAt; - if (durationMs >= SLOW_RECONCILE_MS) { + if (this.diag && durationMs >= SLOW_RECONCILE_MS) { this.log.warn('session index reconciliation slow', { durationMs, generation: manifest.seq, @@ -282,7 +284,7 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { } catch (error) { this.log.warn('session index reconciliation failed', { error: String(error), - durationMs: Date.now() - startedAt, + durationMs: this.diag ? Date.now() - startedAt : undefined, }); } } @@ -703,7 +705,7 @@ export class FileSessionIndex extends Disposable implements ISessionIndex { result = [...byId.values()]; } const durationMs = Date.now() - startedAt; - if (durationMs >= SLOW_AUTHORITATIVE_SCAN_MS) { + if (this.diag && durationMs >= SLOW_AUTHORITATIVE_SCAN_MS) { this.log.warn('session index authoritative scan slow', { durationMs, sessions: result.length, diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 1b339247330..b2170aac853 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -169,6 +169,7 @@ export * from '#/app/sessionIndex/sessionIndex'; export * from '#/app/sessionIndex/sessionIndexService'; export * from '#/app/sessionIndex/sessionIndexMirrorService'; export * from '#/app/diagnostics/eventLoopMonitor'; +export * from '#/app/diagnostics/diag'; export * from '#/session/sessionMetadata/sessionMetadata'; export * from '#/session/sessionMetadata/sessionMetadataService'; export * from '#/session/sessionMetadata/promptMetadata'; diff --git a/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts b/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts index ec19eb4f1d4..49a0fc3850f 100644 --- a/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts +++ b/packages/agent-core-v2/src/persistence/backends/minidb/miniDbQueryStore.ts @@ -22,6 +22,8 @@ import { type WriteOp, } from '#/persistence/interface/queryStore'; +import { rebuildOnContentionEnabled } from '#/app/diagnostics/diag'; + const SEP = String.fromCodePoint(0); const CHECKPOINT_COLLECTION = '__checkpoint__'; const STORE_SUBDIR = 'query-store'; @@ -60,6 +62,7 @@ export class MiniDbQueryStore extends Disposable implements IQueryStore { private transientWriteFailures = 0; private storeEpochCounter = 0; private lastContentionLogAt = 0; + private readonly rebuildOnContention = rebuildOnContentionEnabled(); private readonly ensuredIndexes = new Set(); constructor( @@ -149,7 +152,7 @@ export class MiniDbQueryStore extends Disposable implements IQueryStore { if (classifyStorageError(error) !== 'rebuild') { if (isLockContentionError(error)) { this.noteLockContention(db, kind, error); - throw error; + if (!this.rebuildOnContention) throw error; } const failures = kind === 'write'