kimi-code/packages/server
Kai ace7901066
feat(agent-core): compress oversized images before sending to the model (#1243)
* 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.
2026-07-01 19:36:48 +08:00
..
scratch feat(web): introduce Kimi web app and daemon gateway (#625) 2026-06-17 20:53:46 +08:00
src feat(agent-core): compress oversized images before sending to the model (#1243) 2026-07-01 19:36:48 +08:00
test feat(agent-core): compress oversized images before sending to the model (#1243) 2026-07-01 19:36:48 +08:00
AGENTS.md feat(web): introduce Kimi web app and daemon gateway (#625) 2026-06-17 20:53:46 +08:00
CHANGELOG.md ci: release packages (#1224) 2026-07-01 10:51:54 +08:00
package.json feat(agent-core): compress oversized images before sending to the model (#1243) 2026-07-01 19:36:48 +08:00
README.md feat(web): introduce Kimi web app and daemon gateway (#625) 2026-06-17 20:53:46 +08:00
SECURITY.md feat(server): add --allowed-host flag for DNS-rebinding allowlist (#1128) 2026-06-26 17:06:01 +08:00
tsconfig.json feat(web): introduce Kimi web app and daemon gateway (#625) 2026-06-17 20:53:46 +08:00
tsdown.config.ts feat(web): introduce Kimi web app and daemon gateway (#625) 2026-06-17 20:53:46 +08:00
vitest.config.ts ci: skip server e2e tests on windows (#1126) 2026-06-26 18:01:22 +08:00

@moonshot-ai/server

Local REST + WebSocket server that exposes the Kimi Code SDK over a stable wire protocol. It hosts agent-core sessions and serves them under a single /api/v1 prefix. This package is private — it is not published on its own; it ships inside the kimi CLI (apps/kimi-code) and is launched via kimi server run.

What it does

  • Hosts agent-core sessions, prompts, tools, approvals, questions, and workspaces in process.
  • Exposes them over REST (Fastify) and WebSocket (ws) under /api/v1.
  • Serves the built-in web UI (apps/kimi-web) as static assets when a webAssetsDir is provided.
  • Publishes machine-readable contract docs: /openapi.json, /asyncapi.json.

Running it

# From the repo root — dev server with auto-restart
pnpm dev:server
pnpm dev:server:restart

# Checks
pnpm --filter @moonshot-ai/server typecheck   # tsc --noEmit
pnpm --filter @moonshot-ai/server test        # vitest run
pnpm --filter @moonshot-ai/server build       # tsdown

The public entry point is startServer(opts) in src/start.ts, which returns a RunningServer. In production the CLI command kimi server run (apps/kimi-code/src/cli/sub/server/run.ts) imports and calls it. This package has no dev script of its own — always start it from the repo root or via the CLI.

By default the server listens on 127.0.0.1:58627; e2e clients target it with KIMI_SERVER_URL (default http://127.0.0.1:58627).

Architecture

apps/kimi-code (CLI)            apps/kimi-web (browser)
        │                              │
        └──────────┬───────────────────┘
                   │  REST + WebSocket, /api/v1
        ┌──────────▼───────────┐
        │  @moonshot-ai/server │
        │  Fastify REST        │
        │  ws gateway          │
        │  DI container        │  ← @moonshot-ai/agent-core
        │  agent-core sessions │  ← @moonshot-ai/agent-core
        └──────────────────────┘
  • REST (src/routes/): domain modules aggregated by registerApiV1Routes.ts. Routes are declared with middleware/defineRoute.ts, which bundles Zod validators with the OpenAPI response schema.
  • WebSocket (src/ws/, src/services/gateway/): per-session seq, server_hello / ack / event / resync_required frames, replay and fan-out.
  • DI (src/services/serviceCollection.ts): seeds the container from @moonshot-ai/agent-core (getSingletonServiceDescriptors()) and layers in server-owned gateways plus IApprovalService / IQuestionService implementations.
  • OS service managers (src/svc/): launchd / systemd / schtasks backends for kimi server install/start.

Wire protocol notes

  • Envelope: every REST response is { code, msg, data, request_id } and the HTTP status is effectively always 200 — check code (0 = ok), not the status.
  • :action endpoints: some routes use an :id:action suffix (e.g. /sessions/{id}:undo); the suffix is parsed by routes/action-suffix.ts.
  • Single-instance lock: a running server acquires a lock; a second start on the same home throws ServerLockedError. Tests pass a unique lockPath / port.
  • @moonshot-ai/agent-core — the agent engine the server hosts, including the in-process DI service layer it wires together.
  • @moonshot-ai/protocol — wire types and the AsyncAPI document.
  • @moonshot-ai/node-sdk — typed in-process facade for user code (KimiHarness, Session); prefer it over hand-rolling REST/WS calls.
  • @moonshot-ai/server-e2e — wire-level e2e client and scenarios against a running server.

Development

For conventions, gotchas, and the boot wiring order, see packages/server/AGENTS.md. For the service naming and registration rules, see packages/services/AGENTS.md.