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

Fix multi-second typing and rendering freezes at startup or while idle when a large search index loads, replays, or rebuilds.
12 changes: 8 additions & 4 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,24 @@
}
},
{
// The stage-6 worker closure: these modules (and everything
// packages/minidb/src/worker/ pulls in) are loaded by a bare
// The worker closures: these modules (and everything
// packages/minidb/src/worker/ and
// packages/kap-server/src/search/worker/ pull in) are loaded by a bare
// node:worker_threads Worker under Node's native type stripping with
// `execArgv: ['--experimental-transform-types']`, which requires
// explicit `.ts` import specifiers (the strip loader does not remap
// `.js` -> `.ts`). Keep the exception scoped to exactly that closure.
// `.js` -> `.ts`). Keep the exception scoped to exactly those closures.
"files": [
"packages/minidb/src/worker/**/*.ts",
"packages/minidb/src/codec.ts",
"packages/minidb/src/crc32.ts",
"packages/minidb/src/trigram.ts",
"packages/minidb/src/text-postings.ts",
"packages/minidb/src/text-index/tokenize.ts",
"packages/minidb/src/gen-codec.ts"
"packages/minidb/src/gen-codec.ts",
"packages/kap-server/src/search/worker/**/*.ts",
"packages/kap-server/src/search/indexCore.ts",
"packages/kap-server/src/search/match.ts"
],
"rules": {
"import/extensions": "off"
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo

## Experimental Features

- Gate a not-yet-public feature behind an experimental flag. Add the flag to the registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`.
- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`.
- `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`.
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time (see `packages/agent-core-v2/docs/flag.md`), then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on).

## Where to Update Instructions

Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"provenance": true
},
"scripts": {
"build": "tsdown && node scripts/copy-native-assets.mjs && node scripts/check-web-assets.mjs",
"build": "tsdown && tsdown --config tsdown.dist-worker.config.ts && node scripts/copy-native-assets.mjs && node scripts/check-web-assets.mjs",
"prebuild": "node scripts/build-vis-asset.mjs",
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
"smoke": "node scripts/smoke.mjs",
Expand Down
11 changes: 6 additions & 5 deletions apps/kimi-code/scripts/native/01-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ export async function runBundleStep() {
// miss it (npm builds get it via the `prebuild` script).
await run(process.execPath, [buildVisAssetPath]);
await run(process.execPath, [tsdownCliPath, '--config', 'tsdown.native.config.ts']);
// Bundle the minidb text-build worker into one self-contained ESM file so
// it can ride the SEA blob as an asset (02-sea-blob.mjs) and be spawned
// from disk at runtime — bundled binaries otherwise lack the worker entry
// and heavy text-index builds degrade to the inline main-thread core.
// Runs after the main bundle with clean:false so both verified files remain.
// Bundle the off-main-thread workers (the minidb text-build worker and
// the kap-server global-search worker) into self-contained ESM files so
// they can ride the SEA blob as assets (02-sea-blob.mjs) and be spawned
// from disk at runtime — bundled binaries otherwise lack the worker
// entries and heavy index work degrades to inline main-thread cores.
// Runs after the main bundle with clean:false so all verified files remain.
await run(process.execPath, [tsdownCliPath, '--config', 'tsdown.worker.config.ts']);
await run(process.execPath, [checkBundlePath]);
}
Expand Down
27 changes: 16 additions & 11 deletions apps/kimi-code/scripts/native/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path
import { pathToFileURL } from 'node:url';

import {
KAP_SEARCH_WORKER_ASSET,
MINIDB_TEXT_BUILD_WORKER_ASSET,
NATIVE_ASSET_MANIFEST_VERSION,
buildManifestKey,
Expand Down Expand Up @@ -272,19 +273,23 @@ export async function collectNativeAssets({ appRoot, target }) {
Object.assign(assets, result.assets);
}

const workerSource = resolve(appRoot, 'dist-native', 'intermediates', 'text-build-worker.mjs');
const workerBytes = await readFile(workerSource);
const workerAssetKey = buildRuntimeAssetKey(target, MINIDB_TEXT_BUILD_WORKER_ASSET.key);
const runtimeFiles = [
{
key: MINIDB_TEXT_BUILD_WORKER_ASSET.key,
const runtimeFiles = [];
for (const [fileName, asset] of [
['text-build-worker.mjs', MINIDB_TEXT_BUILD_WORKER_ASSET],
['search-worker.mjs', KAP_SEARCH_WORKER_ASSET],
]) {
const workerSource = resolve(appRoot, 'dist-native', 'intermediates', fileName);
const workerBytes = await readFile(workerSource);
const workerAssetKey = buildRuntimeAssetKey(target, asset.key);
runtimeFiles.push({
key: asset.key,
assetKey: workerAssetKey,
relativePath: MINIDB_TEXT_BUILD_WORKER_ASSET.relativePath,
relativePath: asset.relativePath,
sha256: sha256(workerBytes),
mode: MINIDB_TEXT_BUILD_WORKER_ASSET.mode,
},
];
assets[workerAssetKey] = workerSource;
mode: asset.mode,
});
assets[workerAssetKey] = workerSource;
}

const manifest = {
version: NATIVE_ASSET_MANIFEST_VERSION,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/scripts/native/check-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function checkBundle(bundlePath, { worker = false } = {}) {
const bundles = [
{ path: nativeJsBundlePath(), worker: false },
{ path: resolve(nativeIntermediatesDir(), 'text-build-worker.mjs'), worker: true },
{ path: resolve(nativeIntermediatesDir(), 'search-worker.mjs'), worker: true },
];
let failed = false;
for (const bundle of bundles) {
Expand Down
6 changes: 6 additions & 0 deletions apps/kimi-code/scripts/native/manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export const MINIDB_TEXT_BUILD_WORKER_ASSET = Object.freeze({
mode: 0o644,
});

export const KAP_SEARCH_WORKER_ASSET = Object.freeze({
key: 'kap-search-worker',
relativePath: 'runtime/kap-server/search-worker.mjs',
mode: 0o644,
});

export function buildManifestKey(target) {
return `native/${target}/manifest.json`;
}
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/scripts/native/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ try {
});
assertIncludes(nativeAssetOutput, `Native asset smoke passed: ${target}`, 'native asset smoke');
assertIncludes(nativeAssetOutput, 'MiniDb worker build passed', 'MiniDb worker smoke');
assertIncludes(nativeAssetOutput, 'search worker ready', 'search worker smoke');
} finally {
await rm(smokeHome, { recursive: true, force: true });
}
Expand Down
12 changes: 12 additions & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { createKimiCodeHostIdentity, getVersion } from './cli/version';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, PROCESS_NAME } from './constant/app';
import { cleanupStaleNativeCacheForCurrent } from './native/native-assets';
import { installMinidbTextBuildWorker } from './native/minidb-worker';
import { installKapSearchWorker } from './native/search-worker';
import { installNativeModuleHook } from './native/module-hook';
import { runNativeAssetSmokeIfRequested } from './native/smoke';

Expand Down Expand Up @@ -158,6 +159,17 @@ export function main(): void {
? `minidb-worker:failed code=${workerInstall.errorCode} sha256=${workerInstall.assetSha256 ?? 'unknown'}`
: `minidb-worker:${workerInstall.status}`,
);
// Same pattern for the global-search worker: extracted from the SEA blob so
// the search index runs off the main thread; a failure leaves the search
// surface degraded (the `search_worker` flag restores the inline host).
const searchWorkerInstall = installKapSearchWorker();
startupTrace(
searchWorkerInstall.status === 'installed'
? `search-worker:installed basename=${searchWorkerInstall.basename} sha256=${searchWorkerInstall.assetSha256}`
: searchWorkerInstall.status === 'failed'
? `search-worker:failed code=${searchWorkerInstall.errorCode} sha256=${searchWorkerInstall.assetSha256 ?? 'unknown'}`
: `search-worker:${searchWorkerInstall.status}`,
);
if (runNativeAssetSmokeIfRequested()) return;

// Start the background cleanup of stale native cache. Fire-and-forget; must not block startup or throw.
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/src/native/native-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { join as joinPosix } from 'pathe';

import { KIMI_BUILD_INFO } from '#/cli/build-info';
import {
KAP_SEARCH_WORKER_ASSET,
MINIDB_TEXT_BUILD_WORKER_ASSET,
NATIVE_ASSET_MANIFEST_VERSION as MANIFEST_VERSION,
buildManifestKey,
Expand Down Expand Up @@ -416,6 +417,10 @@ export function getMinidbTextBuildWorkerFile(
return getNativeRuntimeFile(MINIDB_TEXT_BUILD_WORKER_ASSET.key, options);
}

export function getKapSearchWorkerFile(options: NativeAssetOptions = {}): string | null {
return getNativeRuntimeFile(KAP_SEARCH_WORKER_ASSET.key, options);
}

export function getNativePackageRoot(
packageName: string,
options: NativeAssetOptions = {},
Expand Down
72 changes: 72 additions & 0 deletions apps/kimi-code/src/native/search-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { basename } from 'node:path';

import {
configureSearchWorkerRuntime,
getSearchWorkerRuntimeState,
} from '@moonshot-ai/kap-server/search-worker-runtime';

import { KAP_SEARCH_WORKER_ASSET } from '../../scripts/native/manifest.mjs';
import {
getEmbeddedNativeAssetManifest,
getKapSearchWorkerFile,
getSeaAssetSource,
type NativeAssetOptions,
} from './native-assets';

export type KapSearchWorkerInstallStatus =
| { readonly status: 'not-sea' }
| { readonly status: 'asset-missing' }
| {
readonly status: 'installed';
readonly assetSha256: string;
readonly basename: string;
}
| {
readonly status: 'failed';
readonly errorCode: string;
readonly assetSha256?: string;
};

function errorCode(error: unknown): string {
const code = (error as NodeJS.ErrnoException | undefined)?.code;
if (typeof code === 'string' && code.length > 0) return code;
return error instanceof Error ? error.name : 'UNKNOWN';
}

/**
* Install the SEA-bundled global-search worker without making optional
* extraction fatal. Without it the search service resolves no worker entry
* inside the single-file binary and reports the index as degraded; the
* `search_worker` experimental flag restores the in-process host.
*/
export function installKapSearchWorker(
options: NativeAssetOptions = {},
): KapSearchWorkerInstallStatus {
const source = options.source ?? getSeaAssetSource();
if (source === null) return { status: 'not-sea' };

let assetSha256: string | undefined;
try {
const manifest = options.manifest ?? getEmbeddedNativeAssetManifest(source);
const file = manifest?.runtimeFiles.find((entry) => entry.key === KAP_SEARCH_WORKER_ASSET.key);
if (manifest === null || file === undefined) return { status: 'asset-missing' };
assetSha256 = file.sha256;

const workerPath = getKapSearchWorkerFile({ ...options, source, manifest });
if (workerPath === null) return { status: 'asset-missing' };
configureSearchWorkerRuntime(workerPath);
const runtime = getSearchWorkerRuntimeState();
if (!runtime.configured) throw new Error('search worker runtime was not configured');
return {
status: 'installed',
assetSha256,
basename: basename(workerPath),
};
} catch (error) {
return {
status: 'failed',
errorCode: errorCode(error),
assetSha256,
};
}
}
36 changes: 35 additions & 1 deletion apps/kimi-code/src/native/smoke.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { createRequire } from 'node:module';
import { once } from 'node:events';
import { dirname, join } from 'node:path';
import { Worker } from 'node:worker_threads';

import { MiniDb } from '@moonshot-ai/minidb';
import { getSearchWorkerRuntimeState } from '@moonshot-ai/kap-server/search-worker-runtime';

import {
getEmbeddedNativeAssetManifest,
Expand Down Expand Up @@ -74,6 +77,34 @@ async function smokeMinidbWorker(): Promise<void> {
}
}

async function smokeSearchWorker(): Promise<void> {
// The SEA-extracted global-search worker entry must boot from disk and
// complete the versioned ready handshake.
const runtime = getSearchWorkerRuntimeState();
if (!runtime.configured) {
throw new Error('search worker runtime was not configured');
}
const cacheBase = getNativeCacheBase();
mkdirSync(cacheBase, { recursive: true });
const dir = mkdtempSync(join(cacheBase, 'sea-search-worker-'));
const worker = new Worker(runtime.path, {
workerData: { dir, bootSalt: 'sea-smoke' },
});
try {
const ready = once(worker, 'message', {
signal: AbortSignal.timeout(15_000),
}) as Promise<unknown[]>;
const [event] = await ready;
const v = (event as { type?: string; v?: number }).v;
if ((event as { type?: string }).type !== 'ready' || typeof v !== 'number') {
throw new Error(`search worker handshake is unexpected: ${JSON.stringify(event)}`);
}
} finally {
await worker.terminate().catch(() => {});
rmSync(dir, { recursive: true, force: true });
}
}

async function runSmoke(): Promise<void> {
const manifest = getEmbeddedNativeAssetManifest();
if (manifest === null) throw new Error('Native asset manifest is not available.');
Expand All @@ -84,7 +115,10 @@ async function runSmoke(): Promise<void> {
}
smokePiTuiNativeLoad();
await smokeMinidbWorker();
process.stdout.write(`Native asset smoke passed: ${manifest.target}; MiniDb worker build passed\n`);
await smokeSearchWorker();
process.stdout.write(
`Native asset smoke passed: ${manifest.target}; MiniDb worker build passed; search worker ready\n`,
);
}

export function runNativeAssetSmokeIfRequested(): boolean {
Expand Down
41 changes: 41 additions & 0 deletions apps/kimi-code/tsdown.dist-worker.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Bundles the kap-server global-search worker
// (packages/kap-server/src/search/worker/entry.ts) into ONE self-contained
// `dist/search-worker.mjs` sibling of the main bundle. The search worker
// host resolves it at runtime next to `dist/main.mjs` (dev/tests use the TS
// source; the SEA binary uses the extracted asset from
// tsdown.worker.config.ts). Separate config because rolldown forbids
// `codeSplitting: false` with multiple inputs.

import { resolve } from 'node:path';

import { defineConfig } from 'tsdown';

const appRoot = import.meta.dirname;

export default defineConfig({
entry: {
'search-worker': resolve(
appRoot,
'../../packages/kap-server/src/search/worker/entry.ts',
),
},
format: ['esm'],
// Shares the main bundle's dist (never wipe it) and lands as
// `dist/search-worker.mjs`.
outDir: 'dist',
clean: false,
dts: false,
hash: false,
platform: 'node',
target: 'node24',
sourcemap: false,
minify: false,
silent: true,
deps: {
onlyBundle: false,
},
outputOptions: {
codeSplitting: false,
entryFileNames: '[name].mjs',
},
});
Loading
Loading