Skip to content

perf(web): make the kimi web host usable on slow links and bound browser load - #3706

Open
REtoolsx wants to merge 10 commits into
MoonshotAI:mainfrom
REtoolsx:kimi-web-host-performance-5b01eb
Open

perf(web): make the kimi web host usable on slow links and bound browser load#3706
REtoolsx wants to merge 10 commits into
MoonshotAI:mainfrom
REtoolsx:kimi-web-host-performance-5b01eb

Conversation

@REtoolsx

@REtoolsx REtoolsx commented Sep 10, 2026

Copy link
Copy Markdown

Related Issue

No linked issue. This PR comes from a performance review of the kimi web host for users on slow connections and low-end browsers.

Problem

Using the browser UI over a slow or high-latency link was painful, and long sessions could overload the browser:

  • Every static asset was served uncompressed with no ETag, so each page load re-downloaded ~3.9 MB of entry JS/CSS; .wasm/.woff/.ttf fell back to application/octet-stream, which broke WebAssembly.instantiateStreaming under nosniff and made the Rive runtime download twice.
  • The WebSocket sent one JSON envelope per streamed token (roughly 20:1 envelope-to-text overhead), had no compression, and its backpressure force-flushed after 100 ms, so a slow client could never slow the producer.
  • GET /api/v1/sessions applied the busy filter after slicing, so filtered pages came back short with an inaccurate has_more, and the transcript ops catch-up had no cap.
  • Remote Control buffered each response into one base64 frame, stripped Accept-Encoding, and forced Cache-Control: no-cache on every rewritten asset, so hashed assets were re-fetched on every load.

What changed

Static assets (packages/kap-server/src/routes/webAssets.ts)

  • Negotiates precompressed .br/.gz siblings via Accept-Encoding, ranked by the client's q weights with the built-in br-before-gzip order only as a tie-breaker (identity when the header is absent, which keeps the Remote Control rewrite path intact); siblings older than the source are ignored.
  • Weak ETag + If-None-Match → 304, Last-Modified, Vary: Accept-Encoding; complete MIME table; one stat per request.
  • New apps/kimi-code/scripts/precompress-web-assets.mjs runs in pnpm build and in the native bundle workflow (with a --check gate); the siblings are gitignored, never committed.

WebSocket (packages/kap-server/src/transport/ws/v1/)

  • permessage-deflate (no context takeover, 1 KiB threshold) and a 16 MiB maxPayload; server_hello.capabilities.compression now reflects negotiation.
  • Tuning knobs wired from KIMI_CODE_WS_* env vars.
  • Real backpressure: frames wait while bufferedAmount is above the high-water mark; a peer stalled for 15 s or with more than 4096 queued frames is closed with 1013 slow consumer; control frames always flush.
  • Append-only transcript ops are micro-batched (16 ms, KIMI_CODE_TRANSCRIPT_OPS_BATCH_MS) before seq assignment, so one flush = one seq = one envelope and existing clients, which require contiguous seqs, need no change. Journal reads flush first, so REST watermarks stay exact.

REST

  • GET /api/v1/sessions: busy, exclude_empty and archived_only are applied while collecting, so pages fill up to page_size and has_more is accurate. The public default is unchanged: an unsized request still returns the whole list (archived_only keeps its 20-per-page default).
  • GET .../transcript/ops accepts limit (1–500) and reports has_more; complete semantics are unchanged.
  • RunningServer exposes a flags handle so the CLI reads experimental flags through kap-server instead of importing the engine.

Remote Control (packages/remote-control)

  • Upstream cache headers preserved for untouched assets; rewritten HTML/JS/CSS is stored public, no-cache with a versioned ETag so a cheap 304 reuses the browser copy while rewrite-rule changes still invalidate it. 204/304/HEAD pass through bodiless.
  • Reconnect backoff with equal jitter, capped early-frame buffer, pause/resume backpressure on the WebSocket bridge, perMessageDeflate off on the loopback hop. The browser's Sec-WebSocket-* handshake headers are no longer forwarded to the loopback ws client, which used to reject upgrades when the local server accepted permessage-deflate.
  • Chunked responses ship as the remote_control_chunked_responses experimental flag (KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL_CHUNKED_RESPONSES=1, [experimental] config, or KIMI_CODE_EXPERIMENTAL_FLAG=1; default off because the relay contract for multi-frame responses is not verified in this repo). It applies to tunnels started by kimi web --rc, the TUI /rc command and POST /api/v1/remote-control alike.

Docs: server API reference (heartbeat contract corrected, new fields), env vars, Remote Control guide (en/zh). AGENTS.md note about dist-web tracking updated.

Reviewer notes

  • The bundle itself (3.3 MB entry, 7.8 MB CJK font, duplicated mermaid/katex chunks, no modulepreload) lives in code-app and is out of scope here.
  • On Windows the full kap-server suite has pre-existing path-separator/symlink failures; all targeted suites plus transcript, remote-control, kimi-inspect and the new script tests pass, typecheck and check-no-comments are clean.
  • The branch is merged with main after feat(kap-server): add flat entity message protocol (v3 WS + history API) #3532 (flat entity message protocol); the inspector-side catch-up paging from earlier revisions was dropped with that merge because the client no longer uses the transcript ops endpoint.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

…ser load

The browser UI served by `kimi web` shipped every asset uncompressed with
no cache validators, streamed one WebSocket envelope per model token, let
`GET /api/v1/sessions` return every session when `page_size` was omitted,
and forced `no-cache` on assets tunnelled through Remote Control.

Static assets: negotiate precompressed `.br`/`.gz` siblings (generated by
the new precompress script during `pnpm build` and the native bundle
workflow, gitignored), add weak ETag + 304 revalidation and `Vary`, fix
wasm/woff/ttf/riv/map content types, and stat each file once.

WebSocket: enable permessage-deflate and a 16 MiB max payload, expose the
tuning knobs through `KIMI_CODE_WS_*`, replace the 100 ms forced flush
with real backpressure that closes stalled peers with 1013, and always
flush control frames. Append-only transcript ops are micro-batched before
seq assignment so clients keep receiving contiguous seqs.

REST: the sessions list is always paginated (default 50) with `busy`
applied while collecting; the transcript ops catch-up accepts `limit`
and reports `has_more`.

Remote Control: keep upstream cache headers, revalidate rewritten
HTML/JS/CSS through a versioned ETag, pass 204/304/HEAD through
bodiless, add reconnect jitter, cap early frames, apply pause/resume
backpressure on the WebSocket bridge, and add an experimental chunked
response mode behind `KIMI_CODE_REMOTE_CONTROL_CHUNKED_RESPONSES`.
@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e583d36

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5ae21029a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/remote-control/src/remote-control.ts
…local hop

The relay forwards the browser's Sec-WebSocket-Extensions header, but the
loopback ws client runs with permessage-deflate disabled, so when the
local server accepted the advertised extension the client rejected the
upgrade. Strip the browser's handshake fields and let ws negotiate its own.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9818ce4089

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/kap-server/src/routes/sessions.ts Outdated
Comment thread packages/remote-control/src/remote-control.ts Outdated
…ses behind an experimental flag

The always-paginated sessions list silently truncated callers that never
passed page_size, which is a breaking API change under a patch changeset.
Without page_size the listing now returns every eligible session again
(archived_only keeps its historical page of 20); explicit page_size keeps
the collect-while-filtering behaviour and accurate has_more.

Remote Control chunked responses were toggled by a standalone env var that
bypassed KIMI_CODE_EXPERIMENTAL_FLAG and the [experimental] config. The
feature is now the `remote_control_chunked_responses` flag registered
through registerFlagDefinition; `kimi web --rc` and the TUI /rc command
resolve it through IFlagService and pass it to the tunnel as an option.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ee1304589

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/kimi-code/src/cli/sub/web/run.ts Outdated
Comment thread packages/kap-server/src/routes/webAssets.ts Outdated
…tal flags to the CLI

Pre-compressed asset selection now picks the accepted encoding with the
highest q value, falling back to the built-in br-before-gzip order only on
ties. RunningServer gains a `flags` handle so the CLI reads the
remote-control chunked-responses flag through kap-server instead of
importing the engine's IFlagService directly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce7b4dd7d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/kimi-code/src/cli/sub/web/run.ts
…ated tunnels

Tunnels started through POST /api/v1/remote-control go through
createRemoteControlManager, which never passed chunkedResponses. The
manager now takes a chunkedResponses thunk resolved at each tunnel start,
and kap-server wires it to the remote_control_chunked_responses flag via
IFlagService (the manager is created after the engine core bootstraps).
@REtoolsx
REtoolsx force-pushed the kimi-web-host-performance-5b01eb branch from 7cc22b5 to 83d82bd Compare September 10, 2026 11:18
- ws v1: control frames no longer force-flush the deferred backlog above the
  high-water mark, and the slow-consumer clock resets while the peer drains
- transcript ops catch-up: a capped response reports latest_seq as the last
  returned batch so cursors written against the old contract stay correct
- transcript service: pending appends are flushed, not discarded, when a
  session is dropped or purged
- kimi-inspect: seed from one unsized request, drain with before_id only when
  has_more, and keep the pages already collected when a later page fails
- remote-control: the chunked-responses flag is excluded from the
  KIMI_CODE_EXPERIMENTAL_FLAG master switch
- precompress: write siblings atomically, and --check honours the size
  threshold and sibling freshness
- webAssets: stat precompressed siblings in parallel, reuse pickHeader and
  buildEtag, drop the redundant .riv case
- reuse the shared env parsers for KIMI_CODE_WS_* and the ops batch window,
  simplify flushPendingOps, drop the unused --only flag and skip list, and
  share a header lookup in remote-control

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d31b4b988

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/remote-control/src/flag.ts Outdated
…d responses

Drop the excludeFromMaster escape hatch added for remote_control_chunked_responses so
KIMI_CODE_EXPERIMENTAL_FLAG=1 enables it like every other experimental flag. The flag
still defaults to off and the per-flag env var and [experimental] config keep precedence.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be02b158e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +235 to +240
const catchup = transcriptService.getOpsSince(
session_id,
query.agent_id,
query.since_seq,
query.limit ?? MAX_TRANSCRIPT_OPS_LIMIT,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve unsized transcript-ops catch-up semantics

Clients that omit limit previously received every journal batch after since_seq; this now silently returns only the first 500 while leaving complete: true. An existing caller that does not know the newly added has_more field will stop after that response and retain an incomplete transcript without triggering its existing refresh path. Keep the old unbounded behavior when limit is absent, or treat this public API break as a major release.

AGENTS.md reference: AGENTS.md:L64-L64

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e583d36: omitting limit returns every journaled batch after since_seq again (the pre-limit behaviour) with has_more: false; only an explicit limit (1–500) caps the response. The route test now pushes more than 500 batches through an unsized request, and the API reference (en/zh) and the changeset say so.

Omitting limit on GET .../transcript/ops returns every journaled batch
after since_seq again, as it did before limit existed; only an explicit
limit caps the response and sets has_more.
@REtoolsx

Copy link
Copy Markdown
Author

Live test report (before / after)

I ran both builds locally on Windows 11 / Node 25 and exercised them live: main (5b3b5b6f7) on port 43111 as the baseline and this branch (e583d36ac, after pnpm precompress:web) on port 43110, both started through kimi web --no-open. The web UI was driven in a Chromium browser against the branch build (onboarding → session → prompt → streamed answer with thinking rendered), the WebSocket was measured with a small ws client that subscribes with subscribe_v2 (transcript: delta) and submits the same prompt to both servers, and the HTTP paths were probed with curl.

Area Check Before (main) After (this PR)
Static assets GET / with Accept-Encoding: br, gzip 1187 B, identity, no ETag/Vary/Last-Modified 423 B, content-encoding: br, weak ETag, Vary: Accept-Encoding, Last-Modified
Static assets Entry JS index-HU0LCM-X.js on the wire 3,310,837 B 851,200 B (br) / 1,086,082 B (gz)
Static assets Entry CSS index-C8RkgE6U.css on the wire 568,857 B 73,643 B (br)
Static assets rive-*.wasm on the wire 1,941,759 B 559,751 B (br)
Static assets Whole first paint set (html, boot.js, entry js+css, 2 workers, wasm) ≈ 6.74 MB ≈ 1.68 MB with br (fonts unchanged)
Static assets Revalidation with If-None-Match 200 + full 370 KB body again 304, 0 B body
Static assets Accept-Encoding: gzip;q=1, br;q=0.1 n/a (identity) serves gzip (client q wins, br only as tie-breaker)
Static assets No Accept-Encoding header identity identity (Remote Control rewrite path unchanged)
MIME .wasm / .ttf application/octet-stream application/wasm / font/ttf (.woff2 already font/woff2)
WebSocket server_hello.capabilities.compression / negotiated extension false / none true / permessage-deflate
WebSocket Frames for one ~300-char streamed answer (same prompt, kimi-k2.7-code-highspeed) 186 frames, 180 transcript.ops, 167 append ops, 73,212 B JSON, 74,171 B on the wire 25 frames, 19 transcript.ops, 6 append ops, 12,897 B JSON, 12,716 B on the wire
REST GET /api/v1/sessions?page_size=3&busy=true (no busy session exists) items: [], has_more: true (filter applied after slicing) items: [], has_more: false
REST GET /api/v1/sessions without page_size 16 items, has_more: false 16 items, has_more: false (default unchanged)
REST GET .../transcript/ops?since_seq=0&limit=3 181 batches, limit ignored, no has_more 3 batches, has_more: true, latest_seq: 3, complete: true
REST GET .../transcript/ops?since_seq=0 (no limit) every batch every batch, has_more: false (kept unbounded, see below)
REST GET .../transcript/ops?limit=0 accepted 40001 validation error

Notes:

  • The browser session (onboarding, session switch, prompt, streamed reply with the thinking block) worked end to end on the branch build; the streamed text arrived in micro-batched transcript.ops frames and the seqs stayed contiguous.
  • precompress-web-assets.mjs produced 852 siblings for 426 files (24.0 MB → 5.1 MB) and reports them as up to date on a second run.
  • Not exercised live: the Remote Control tunnel (kimi web --rc, chunked responses) — it needs a signed-in Kimi account and the relay, so it is covered by the package tests only.
  • Follow-up from the latest Codex comment: the unsized transcript/ops request is unbounded again (e583d36ac), with a route test that pushes >500 batches through it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant