Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .agents/skills/agent-core-dev/edge-exposure.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ Read = `GET`, write = `POST`. `sid` = `session_id`, `aid` = `agent_id`.

| resource | action | Service.method | verb |
|---|---|---|---|
| `sessions` | `list` | ISessionIndex.list | GET |
| `sessions` | `listRecent` | ISessionIndex.listRecent | GET |
| `sessions` | `get` | ISessionIndex.get | GET |
| `sessions` | `countActive` | ISessionIndex.countActive | GET |
| `sessions` | `count` | ISessionIndex.count | GET |
| `workspaces` | `list` | IWorkspaceService.list | GET |
| `workspaces` | `get` | IWorkspaceService.get | GET |
| `workspaces` | `createOrTouch` | IWorkspaceService.createOrTouch | POST |
Expand Down
20 changes: 20 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@
"eslint/no-console": "off"
}
},
{
// The stage-6 worker closure: these modules (and everything
// packages/minidb/src/worker/ pulls 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.
"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"
],
"rules": {
"import/extensions": "off"
}
},
{
"files": ["packages/kosong/src/providers/**/*.ts"],
"rules": {
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/kimi-code/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ agents/
src/generated/vis-web-asset.ts

# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
native/
/native/
1 change: 1 addition & 0 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"@moonshot-ai/kimi-telemetry": "workspace:^",
"@moonshot-ai/kimi-web": "workspace:^",
"@moonshot-ai/migration-legacy": "workspace:^",
"@moonshot-ai/minidb": "workspace:^",
"@moonshot-ai/pi-tui": "workspace:^",
"@moonshot-ai/vis-server": "workspace:^",
"@moonshot-ai/vis-web": "workspace:*",
Expand Down
6 changes: 6 additions & 0 deletions apps/kimi-code/scripts/native/01-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +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.
await run(process.execPath, [tsdownCliPath, '--config', 'tsdown.worker.config.ts']);
await run(process.execPath, [checkBundlePath]);
}

Expand Down
27 changes: 25 additions & 2 deletions apps/kimi-code/scripts/native/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { createRequire } from 'node:module';
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

import { NATIVE_ASSET_MANIFEST_VERSION, buildManifestKey } from './manifest.mjs';
import {
MINIDB_TEXT_BUILD_WORKER_ASSET,
NATIVE_ASSET_MANIFEST_VERSION,
buildManifestKey,
buildRuntimeAssetKey,
} from './manifest.mjs';
import { resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs';

export { NATIVE_ASSET_MANIFEST_VERSION };
Expand Down Expand Up @@ -229,7 +234,10 @@ async function packageManifestEntries({ packageName, packageRoot, files, target
export const nativeAssetManifestKey = buildManifestKey;

export function nativeAssetSummary(manifest) {
return manifest.packages.map((pkg) => `${pkg.name}: ${pkg.files.length} files`);
return [
...manifest.packages.map((pkg) => `${pkg.name}: ${pkg.files.length} files`),
`runtime: ${manifest.runtimeFiles.length} files`,
];
}

export async function collectNativeAssets({ appRoot, target }) {
Expand Down Expand Up @@ -264,10 +272,25 @@ 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,
assetKey: workerAssetKey,
relativePath: MINIDB_TEXT_BUILD_WORKER_ASSET.relativePath,
sha256: sha256(workerBytes),
mode: MINIDB_TEXT_BUILD_WORKER_ASSET.mode,
},
];
assets[workerAssetKey] = workerSource;

const manifest = {
version: NATIVE_ASSET_MANIFEST_VERSION,
target,
packages: manifestPackages,
runtimeFiles,
};

return {
Expand Down
89 changes: 40 additions & 49 deletions apps/kimi-code/scripts/native/check-bundle.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { existsSync, readFileSync } from 'node:fs';
import { builtinModules } from 'node:module';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { nativeJsBundlePath } from './paths.mjs';

const bundlePath = nativeJsBundlePath();
const text = readFileSync(bundlePath, 'utf-8');
import { nativeIntermediatesDir, nativeJsBundlePath } from './paths.mjs';

const builtins = new Set([
...builtinModules,
Expand All @@ -23,18 +21,8 @@ const optionalRuntimeRequires = new Set([
'utf-8-validate',
]);
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
const handledNativeRuntimeRequires = new Set();

function isAllowedSpecifier(specifier) {
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;
if (optionalRuntimeRequires.has(specifier)) return true;
if (handledNativeRuntimeRequires.has(specifier)) return true;
return false;
}

const errors = [];

function executableLines() {
function executableLines(text) {
return text
.split('\n')
.map((line) => line.trim())
Expand All @@ -45,48 +33,51 @@ function executableLines() {
});
}

for (const line of executableLines()) {
for (const match of line.matchAll(/(?<![.\w])require\(\s*["']([^"']+)["']\s*\)/g)) {
const specifier = match[1];
function checkBundle(bundlePath, { worker = false } = {}) {
if (!existsSync(bundlePath)) return [`bundle does not exist: ${bundlePath}`];
const text = readFileSync(bundlePath, 'utf-8');
const errors = [];
const allowedExternal = worker ? new Set() : optionalRuntimeRequires;
const allowedRelative = worker ? new Set() : optionalRelativeRuntimeRequires;

const checkSpecifier = (specifier, kind) => {
if (specifier.startsWith('.') || specifier.startsWith('/')) {
if (optionalRelativeRuntimeRequires.has(specifier)) continue;
errors.push(`relative require remains: ${specifier}`);
continue;
if (!allowedRelative.has(specifier)) errors.push(`relative ${kind} remains: ${specifier}`);
return;
}
if (!isAllowedSpecifier(specifier)) {
errors.push(`external require remains: ${specifier}`);
if (!builtins.has(specifier) && !specifier.startsWith('node:') && !allowedExternal.has(specifier)) {
errors.push(`external ${kind} remains: ${specifier}`);
}
}
};

for (const match of line.matchAll(/(?<![.\w])import\(\s*["']([^"']+)["']\s*\)/g)) {
const specifier = match[1];
if (specifier.startsWith('.') || specifier.startsWith('/')) {
errors.push(`relative dynamic import remains: ${specifier}`);
continue;
for (const line of executableLines(text)) {
for (const match of line.matchAll(/(?<![.\w])require\(\s*["']([^"']+)["']\s*\)/g)) {
checkSpecifier(match[1], 'require');
}
if (!isAllowedSpecifier(specifier)) {
errors.push(`external dynamic import remains: ${specifier}`);
for (const match of line.matchAll(/(?<![.\w])import\(\s*["']([^"']+)["']\s*\)/g)) {
checkSpecifier(match[1], 'dynamic import');
}
}

if (line.startsWith('import ')) {
for (const match of line.matchAll(/\bfrom\s+["']([^"']+)["']/g)) {
const specifier = match[1];
if (specifier.startsWith('.') || specifier.startsWith('/')) {
errors.push(`relative import remains: ${specifier}`);
continue;
}
if (!isAllowedSpecifier(specifier)) {
errors.push(`external import remains: ${specifier}`);
if (line.startsWith('import ')) {
for (const match of line.matchAll(/\bfrom\s+["']([^"']+)["']/g)) {
checkSpecifier(match[1], 'import');
}
const sideEffect = line.match(/^import\s*["']([^"']+)["']/);
if (sideEffect) checkSpecifier(sideEffect[1], 'import');
}
}
return errors;
}

if (errors.length > 0) {
console.error(`Native JS bundle check failed for ${bundlePath}:`);
for (const error of errors) {
console.error(`- ${error}`);
}
process.exit(1);
const bundles = [
{ path: nativeJsBundlePath(), worker: false },
{ path: resolve(nativeIntermediatesDir(), 'text-build-worker.mjs'), worker: true },
];
let failed = false;
for (const bundle of bundles) {
const errors = checkBundle(bundle.path, { worker: bundle.worker });
if (errors.length === 0) continue;
failed = true;
console.error(`Native JS bundle check failed for ${bundle.path}:`);
for (const error of errors) console.error(`- ${error}`);
}
if (failed) process.exit(1);
12 changes: 11 additions & 1 deletion apps/kimi-code/scripts/native/manifest.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
export const NATIVE_ASSET_MANIFEST_VERSION = 1;
export const NATIVE_ASSET_MANIFEST_VERSION = 2;
export const WEB_ASSET_MANIFEST_VERSION = 1;

export const MINIDB_TEXT_BUILD_WORKER_ASSET = Object.freeze({
key: 'minidb-text-build-worker',
relativePath: 'runtime/minidb/text-build-worker.mjs',
mode: 0o644,
});

export function buildManifestKey(target) {
return `native/${target}/manifest.json`;
}

export function buildRuntimeAssetKey(target, key) {
return `native/${target}/runtime/${key}`;
}

export function isManifestVersionSupported(version) {
return version === NATIVE_ASSET_MANIFEST_VERSION;
}
Expand Down
21 changes: 15 additions & 6 deletions apps/kimi-code/scripts/native/smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execFile } from 'node:child_process';
import { readFile, stat } from 'node:fs/promises';
import { mkdir, readFile, rm, stat } from 'node:fs/promises';
import { resolve } from 'node:path';
import { promisify } from 'node:util';

Expand Down Expand Up @@ -73,10 +73,19 @@ assertIncludes(helpOutput, 'Usage: kimi', '--help');
const exportHelpOutput = await runKimi(['export', '--help']);
assertIncludes(exportHelpOutput, 'Usage: kimi export', 'export --help');

const nativeAssetOutput = await runKimiWithEnv(['--version'], {
KIMI_CODE_HOME: smokeHome,
KIMI_CODE_NATIVE_ASSET_SMOKE: '1',
});
assertIncludes(nativeAssetOutput, `Native asset smoke passed: ${target}`, 'native asset smoke');
const smokeCache = resolve(smokeHome, 'cache');
await rm(smokeHome, { recursive: true, force: true });
await mkdir(smokeCache, { recursive: true });
try {
const nativeAssetOutput = await runKimiWithEnv(['--version'], {
KIMI_CODE_CACHE_DIR: smokeCache,
KIMI_CODE_HOME: smokeHome,
KIMI_CODE_NATIVE_ASSET_SMOKE: '1',
});
assertIncludes(nativeAssetOutput, `Native asset smoke passed: ${target}`, 'native asset smoke');
assertIncludes(nativeAssetOutput, 'MiniDb worker build passed', 'MiniDb worker smoke');
} finally {
await rm(smokeHome, { recursive: true, force: true });
}

console.log(`Native smoke passed: ${executablePath}`);
5 changes: 5 additions & 0 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { TuiConfig } from '#/tui/config';
import { loadTuiConfig, TuiConfigParseError } from '#/tui/config';
import { CHROME_GUTTER } from '#/tui/constant/rendering';
import { KimiTUI } from '#/tui/index';
import { startupTrace } from '#/utils/startup-trace';
import { currentTheme, getColorPalette } from '#/tui/theme';
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
import { restoreTerminalModes } from '#/utils/terminal-restore';
Expand Down Expand Up @@ -87,6 +88,7 @@ export async function runShell(
const harness = engineV2
? createKimiHarnessV2(harnessOptions)
: createKimiHarness(harnessOptions);
startupTrace('harness:created');
log.info('kimi-code starting', {
version,
uiMode: CLI_UI_MODE,
Expand All @@ -107,6 +109,7 @@ export async function runShell(
return;
}
const config = await harness.getConfig();
startupTrace('config:loaded');
// Config diagnostics (deprecated keys, invalid sections, ...) are surfaced
// by the TUI itself at `finishStartup` via `showConfigWarningsIfAny` —
// folded into the dim startup notice they were too easy to miss.
Expand Down Expand Up @@ -243,7 +246,9 @@ export async function runShell(
};
try {
const initStartedAt = Date.now();
startupTrace('tui.start:begin');
await tui.start();
startupTrace('tui.start:end');
const initMs = Date.now() - initStartedAt;
const startupSessionId = tui.getCurrentSessionId();
const mcpMs = await tui.getStartupMcpMs();
Expand Down
5 changes: 2 additions & 3 deletions apps/kimi-code/src/cli/v2/run-v2-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,7 @@ async function resolveNativeSession(
};

if (opts.session !== undefined) {
const page = await index.list({});
const target = page.items.find((summary) => summary.id === opts.session);
const target = await index.get(opts.session);
if (target === undefined) {
throw new Error(`Session "${opts.session}" not found.`);
}
Expand All @@ -358,7 +357,7 @@ async function resolveNativeSession(
}

if (opts.continue) {
const page = await index.list({});
const page = await index.listRecent({});
const previous = page.items.find((summary) => summary.cwd === workDir);
if (previous !== undefined) {
const session = await resumeById(previous.id);
Expand Down
Loading
Loading