mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
* feat(agent-core): compress oversized images before sending to the model
Downsample images to a 2000px longest-edge and per-image byte budget at the
single prompt-ingestion chokepoint (the prompt/steer RPC) and on tool results
(ReadMediaFile, MCP), so every client transport — CLI, web, desktop, ACP, SDK —
is covered uniformly inside the core. PNG screenshots stay lossless and only
degrade to JPEG when the byte budget cannot otherwise be met. Best-effort: the
original image is sent unchanged if compression fails.
* fix(agent-core): serialize prompt/steer RPCs to avoid a turn-claim race
The prompt/steer RPC handlers await image compression before turn.launch()
synchronously claims the active turn, so two overlapping calls could both
compress first — letting the faster-to-compress one win the turn and strand the
other on agent_busy. Run these two RPCs through a per-agent serialization chain
so they claim in submit order; cancel and the other RPCs stay immediate.
* fix: update flake.nix pnpmDeps hash for the jimp dependency
Adding jimp to the workspace changed pnpm-lock.yaml, so the pnpmDeps
fixed-output hash was stale and the nix build failed. Update it to the value
the CI nix build reported.
* fix(agent-core): guard image compression against decompression bombs
A tiny-byte, huge-dimension image (e.g. a solid 30000x30000 PNG) would be fully
decoded into a multi-gigabyte bitmap by Jimp before any resize — an OOM vector
the byte budget never catches. Skip compression when the sniffed pixel count
exceeds MAX_DECODE_PIXELS (~100 MP), before the decode; oversized images pass
through uncompressed as they did before compression existed.
* fix(agent-core): cap decode byte size before compressing images
Compression runs before downstream size caps (e.g. the 10MB MCP per-part
limit), so a huge or invalid base64 image from an MCP tool was Buffer.from-
decoded — and handed to Jimp — just to be dropped afterward. Add a
MAX_DECODE_BYTES ceiling (64MB, overridable) checked before the base64 decode
and before Jimp, the byte-side complement to the pixel-count guard; oversized
payloads pass through uncompressed.
* refactor(agent-core): compress images at ingestion, not on the turn RPC
Move image compression off the prompt/steer RPC path and back to each ingestion
site (CLI paste, server upload resolution, ACP conversion; ReadMediaFile and MCP
already compressed at their producers). Compressing on the RPC control path put
an async step before the synchronous turn-claim, which spawned a series of
races: prompt/steer interleaving, and — with a cancel arriving mid-compression —
an ineffective abort that let a cancelled prompt launch anyway.
Treating compression as a pure input-stage transform (done while the content
part is built, before it ever enters the agent loop) removes those races
structurally: rpc.prompt/steer are plain synchronous handlers again, and the
serialization/cancel-window machinery is gone. Records stay compressed, resume
stays consistent, and coverage degrades gracefully (a new client that skips
compression just sends a larger image, as before this feature).
* fix: compress inline base64 prompts and honor ACP cancels mid-compression
Two contained ingestion-site follow-ups:
- server: resolvePromptMediaFiles now also compresses images submitted as an
inline `{ kind: 'base64' }` source, not just uploaded files, so the REST
inline-base64 path gets the same downsampling.
- acp-adapter: AcpSession tracks a pending-abort flag while prompt() awaits
image compression (before any turn exists). A session/cancel in that window
flips it, so the prompt returns `cancelled` instead of launching a turn the
client already stopped.
* fix(acp-adapter): cover all concurrent pre-turn prompts on cancel
The pending-abort marker was a single session field, so with two
`session/prompt` requests compressing large inline images at once the later
one overwrote it and a `session/cancel` could mark only one — the other
launched after the client had cancelled. Track a token per in-flight prompt in
a set and flip them all on cancel so every pre-turn prompt is covered.
* chore(node-sdk): declare jimp as a devDependency
The SDK re-exports the image compressor, whose lazy `import('jimp')` (inside
the bundled agent-core code) is inlined into the published dist. jimp was
resolved only transitively via agent-core, so declare it as an explicit build
input here — matching the CLI — to make the bundling reliable rather than
phantom. It stays a devDependency: jimp is bundled, not a runtime dependency.
255 lines
8.3 KiB
Nix
255 lines
8.3 KiB
Nix
{
|
|
description = "Kimi Code CLI";
|
|
|
|
inputs = {
|
|
# Pinned to the 25.11 release channel because nixpkgs-unstable currently
|
|
# ships nodejs_24 = 24.14.1, which trips the >= 24.15.0 floor that the
|
|
# native SEA build enforces (see apps/kimi-code/scripts/native/build.mjs).
|
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
|
};
|
|
|
|
outputs =
|
|
{ self, nixpkgs }:
|
|
let
|
|
lib = nixpkgs.lib;
|
|
|
|
systems = [
|
|
"x86_64-linux"
|
|
"aarch64-linux"
|
|
"x86_64-darwin"
|
|
"aarch64-darwin"
|
|
];
|
|
|
|
forAllSystems =
|
|
f:
|
|
lib.genAttrs systems (
|
|
system:
|
|
f (import nixpkgs {
|
|
inherit system;
|
|
})
|
|
);
|
|
|
|
minNodeVersion = "24.15.0";
|
|
|
|
# Hardcode to Node.js 24.x; fail the evaluation if the pinned nixpkgs
|
|
# does not offer a new enough 24.x.
|
|
nodejsFor =
|
|
pkgs:
|
|
let
|
|
node = pkgs.nodejs_24;
|
|
in
|
|
if lib.versionAtLeast node.version minNodeVersion then
|
|
node
|
|
else
|
|
throw ''
|
|
Kimi Code requires Node.js >= ${minNodeVersion},
|
|
but nixpkgs only offers ${node.version}.
|
|
Pin a newer nixpkgs revision or update minNodeVersion in flake.nix.
|
|
'';
|
|
|
|
pnpmFor =
|
|
pkgs:
|
|
pkgs.pnpm_10.override {
|
|
nodejs = nodejsFor pkgs;
|
|
};
|
|
|
|
# -------------------------------------------------------------------
|
|
# Workspace members (kept in sync with pnpm-workspace.yaml).
|
|
#
|
|
# HARD REQUIREMENT: whenever you add or remove a workspace package,
|
|
# you MUST update both lists below. Missing a path will break the Nix
|
|
# build (src fileset silently drops files); missing a name will break
|
|
# pnpmConfigHook (dependencies for that workspace won't be fetched).
|
|
# -------------------------------------------------------------------
|
|
workspacePaths = [
|
|
./packages/acp-adapter
|
|
./packages/agent-core
|
|
./packages/server
|
|
./packages/server-e2e
|
|
./packages/kaos
|
|
./packages/kimi-migration-legacy
|
|
./packages/kosong
|
|
./packages/migration-legacy
|
|
./packages/node-sdk
|
|
./packages/oauth
|
|
./packages/protocol
|
|
./packages/telemetry
|
|
./apps/kimi-code
|
|
./apps/kimi-desktop
|
|
./apps/kimi-web
|
|
./apps/vis
|
|
./apps/vis/server
|
|
./apps/vis/web
|
|
./docs
|
|
];
|
|
|
|
workspaceNames = [
|
|
"@moonshot-ai/acp-adapter"
|
|
"@moonshot-ai/agent-core"
|
|
"@moonshot-ai/server"
|
|
"@moonshot-ai/server-e2e"
|
|
"@moonshot-ai/kaos"
|
|
"@moonshot-ai/kosong"
|
|
"@moonshot-ai/migration-legacy"
|
|
"@moonshot-ai/kimi-code-sdk"
|
|
"@moonshot-ai/kimi-code-oauth"
|
|
"@moonshot-ai/protocol"
|
|
"@moonshot-ai/kimi-telemetry"
|
|
"@moonshot-ai/kimi-code"
|
|
"@moonshot-ai/kimi-desktop"
|
|
"@moonshot-ai/kimi-web"
|
|
"@moonshot-ai/vis"
|
|
"@moonshot-ai/vis-server"
|
|
"@moonshot-ai/vis-web"
|
|
"kimi-code-docs"
|
|
"kimi-migration-legacy"
|
|
];
|
|
in
|
|
{
|
|
packages = forAllSystems (
|
|
pkgs:
|
|
let
|
|
nodejs = nodejsFor pkgs;
|
|
pnpm = pnpmFor pkgs;
|
|
appPackageJson = builtins.fromJSON (builtins.readFile ./apps/kimi-code/package.json);
|
|
nativeTarget =
|
|
if pkgs.stdenv.hostPlatform.isLinux && pkgs.stdenv.hostPlatform.isAarch64 then
|
|
"linux-arm64"
|
|
else if pkgs.stdenv.hostPlatform.isLinux then
|
|
"linux-x64"
|
|
else if pkgs.stdenv.hostPlatform.isDarwin && pkgs.stdenv.hostPlatform.isAarch64 then
|
|
"darwin-arm64"
|
|
else if pkgs.stdenv.hostPlatform.isDarwin then
|
|
"darwin-x64"
|
|
else
|
|
throw "Unsupported Kimi Code native target for ${pkgs.stdenv.hostPlatform.system}";
|
|
|
|
kimi-code = pkgs.stdenv.mkDerivation (finalAttrs: {
|
|
pname = "kimi-code";
|
|
version = appPackageJson.version;
|
|
|
|
src = lib.fileset.toSource {
|
|
root = ./.;
|
|
fileset = lib.fileset.unions (
|
|
[
|
|
./build
|
|
./.npmrc
|
|
./.nvmrc
|
|
./package.json
|
|
./pnpm-lock.yaml
|
|
./pnpm-workspace.yaml
|
|
./tsconfig.json
|
|
./vitest.config.ts
|
|
./LICENSE
|
|
]
|
|
++ workspacePaths
|
|
);
|
|
};
|
|
|
|
pnpmWorkspaces = [ "." ] ++ workspaceNames;
|
|
|
|
pnpmDeps = pkgs.fetchPnpmDeps {
|
|
inherit (finalAttrs) pname version src pnpmWorkspaces;
|
|
inherit pnpm;
|
|
fetcherVersion = 3;
|
|
hash = "sha256-mqyi0VuPZwESZcdU5E8F3XUG99OH636knBfb8y6TQpw=";
|
|
};
|
|
|
|
nativeBuildInputs = [
|
|
nodejs
|
|
pnpm
|
|
(pkgs.pnpmConfigHook.override { inherit pnpm; })
|
|
pkgs.makeWrapper
|
|
]
|
|
# The SEA inject step (postject) invalidates the macOS code
|
|
# signature on the copied Node executable; build.mjs then re-applies
|
|
# an ad-hoc signature via `codesign`. The Nix darwin sandbox does
|
|
# not expose /usr/bin/codesign, so we supply nixpkgs' ad-hoc-only
|
|
# replacement instead.
|
|
++ lib.optionals pkgs.stdenv.hostPlatform.isDarwin [
|
|
pkgs.darwin.sigtool
|
|
];
|
|
|
|
# The SEA binary is produced by `postject`-injecting a blob into a
|
|
# plain Node executable. Stripping rewrites section tables and can
|
|
# invalidate the injected blob's offsets, so leave the binary
|
|
# untouched after the build.
|
|
dontStrip = true;
|
|
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
export KIMI_CODE_BUILD_TARGET=${nativeTarget}
|
|
${lib.optionalString pkgs.stdenv.hostPlatform.isDarwin ''
|
|
# pkgs.darwin.sigtool's codesign supports `--sign -` (ad-hoc)
|
|
# but not the inspection mode (`-dv`) that 05-verify.mjs runs
|
|
# afterwards. Disable the verify step for the Nix build; the
|
|
# release CI keeps it via the unmodified script.
|
|
substituteInPlace apps/kimi-code/scripts/native/build.mjs \
|
|
--replace-fail \
|
|
"await runVerifyStep({ requireGatekeeper: false });" \
|
|
"// runVerifyStep skipped in nix sandbox (sigtool lacks -dv)"
|
|
''}
|
|
# The SEA blob step (scripts/native/02-sea-blob.mjs) embeds the
|
|
# Kimi web assets from apps/kimi-code/dist-web and fails if that
|
|
# directory is missing. Build the web app and stage its assets
|
|
# before producing the native executable.
|
|
pnpm --filter=@moonshot-ai/kimi-web run build
|
|
node apps/kimi-code/scripts/copy-web-assets.mjs
|
|
pnpm --filter=@moonshot-ai/kimi-code run build:native:sea
|
|
runHook postBuild
|
|
'';
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
|
|
install -Dm755 \
|
|
"apps/kimi-code/dist-native/bin/${nativeTarget}/kimi" \
|
|
"$out/bin/kimi"
|
|
|
|
runHook postInstall
|
|
'';
|
|
|
|
postInstall = ''
|
|
wrapProgram $out/bin/kimi --prefix PATH : ${lib.makeBinPath [ pkgs.ripgrep pkgs.fd ]}
|
|
'';
|
|
|
|
meta = {
|
|
description = "Kimi Code CLI";
|
|
homepage = "https://github.com/MoonshotAI/kimi-code";
|
|
license = lib.licenses.mit;
|
|
mainProgram = "kimi";
|
|
platforms = systems;
|
|
};
|
|
});
|
|
in
|
|
{
|
|
inherit kimi-code;
|
|
default = kimi-code;
|
|
}
|
|
);
|
|
|
|
apps = forAllSystems (pkgs: {
|
|
kimi-code = {
|
|
type = "app";
|
|
program = "${self.packages.${pkgs.system}.kimi-code}/bin/kimi";
|
|
};
|
|
default = self.apps.${pkgs.system}.kimi-code;
|
|
});
|
|
|
|
devShells = forAllSystems (pkgs: {
|
|
default =
|
|
let
|
|
nodejs = nodejsFor pkgs;
|
|
pnpm = pnpmFor pkgs;
|
|
in
|
|
pkgs.mkShell {
|
|
packages = [
|
|
nodejs
|
|
pnpm
|
|
pkgs.ripgrep
|
|
pkgs.fd
|
|
];
|
|
};
|
|
});
|
|
};
|
|
}
|