Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/minidb-open-lifecycle-slicing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/minidb": patch
---

Slice the open path (generation image load, integrity verification, WAL-delta apply) into cooperative chunks and report a per-phase open lifecycle status, so opening a large database no longer blocks the host event loop; worker-build slot pressure now queues for a bounded wait before falling back to an in-thread build.
5 changes: 5 additions & 0 deletions .changeset/search-index-worker-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix typing and rendering freezing for seconds at startup or while idle when a large global search index loads, replays, or compacts; that work now runs in a dedicated worker instead of on the main thread.
5 changes: 5 additions & 0 deletions .changeset/session-index-read-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Keep the session list, `kimi --resume`, and `kimi --continue` fast and available while the full-text search index is still building, unavailable, or corrupt; session listings are served from their own persistent index.
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