Skip to content

fix(kap-server): gzip API responses and bound the live task output tail - #3883

Open
REtoolsx wants to merge 1 commit into
MoonshotAI:mainfrom
REtoolsx:issue-3881-fix-da37a9
Open

REtoolsx wants to merge 1 commit into
MoonshotAI:mainfrom
REtoolsx:issue-3881-fix-da37a9

Conversation

@REtoolsx

Copy link
Copy Markdown

Related Issue

Resolve #3881

Problem

See linked issue. GET /sessions/{id}/transcript on a large live session can take longer than the web client's single 30 s request budget to deliver its body. Two server-side factors make the body larger than it needs to be:

  • kap-server sends every /api/v1 JSON response uncompressed. Transcript JSON (tool outputs, thinking, prompts) compresses roughly 3× with gzip, and much more when tool output is repetitive. The Remote Control tunnel already gzips what it forwards, but direct LAN / --host access and the local origin got no compression at all.
  • In the live transcript path, a shell task's outputTail grew without bound: every shell.output chunk was appended to the in-memory task and re-sent in full inside the unpaginated tasks collection of every baseline response, regardless of page_size. The v1 projection (agentProjector) already capped this field at 8 KiB; the transcript projection did not.

The remaining part of the issue — the web client's single 30 s budget for headers plus body and the endless identical retries — lives in the web UI source (code-app), which is not in this repository. This PR makes the server-side payload small enough that the budget is far less likely to be hit, and bounds the one collection that could grow indefinitely.

What changed

  • packages/kap-server/src/middleware/compression.ts (new): an onSend hook that gzips string/Buffer responses of JSON/text content types ≥ 1 KiB when the request's Accept-Encoding allows gzip. It skips 204/206/304 and already-encoded responses, sets Content-Encoding: gzip + the compressed Content-Length, and adds Vary: Accept-Encoding (merged with an existing Vary). Compression is synchronous (gzipSync, level 1) on purpose: this codebase's async route handlers call reply.send(...) without returning the reply, and Fastify's wrapThenable re-sends an empty body if an onSend hook is still pending (or returns a stream that has not ended) when the handler promise resolves — an async hook produced content-length: 0 responses in a live test. Level 1 keeps a 30 MB body under ~0.2 s on the event loop while still giving ~2.7× on real transcript JSON.
  • packages/kap-server/src/start.ts: register the hook after the request-logging and security-header hooks. The Remote Control tunnel strips Accept-Encoding before forwarding to the local server, so tunnel traffic is unaffected (no double compression, and the tunnel's HTML/JS prefix rewrite still sees plain bodies). Node clients using http.request without Accept-Encoding keep receiving identity bodies; fetch/undici decompresses transparently.
  • packages/kap-server/src/services/transcript/coreEventMap.ts: onShellOutput keeps appending (append ops) while the tail is ≤ 16 KiB; once it would exceed that, the tail is trimmed to the last 8 KiB and re-synced with a task.upsert carrying the trimmed outputTail, so subsequent append offsets stay consistent for every subscriber. The hysteresis (16 KiB bound, 8 KiB keep) avoids sending an 8 KiB upsert for every chunk once the cap is reached.
  • Tests: test/compression.test.ts (negotiation, type/size/status gating, Vary merging, round-trip via app.inject) and a new case in test/services/transcript.test.ts covering the tail bound and the re-sync upsert.

Live before/after check on a kimi web dev server, through a proxy that throttles /api/* responses (same synthetic session, one turn with 300 tool calls of non-repetitive output, same web bundle):

Session (transcript JSON) Link Before (uncompressed) After (gzip, ~3x)
300 x 10 KB -> 3.2 MB 80 KB/s aborted at 30 s, empty session, transcript baseline retry failed loop loaded in 13.1 s (1.07 MB on the wire), rendered
300 x 50 KB -> 15.8 MB 400 KB/s aborted at 30 s, empty session loaded in 13.2 s (5.3 MB on the wire), rendered
300 x 50 KB -> 15.8 MB 80 KB/s aborted still aborted (5.3 MB needs ~67 s)

So the change roughly triples the transcript size a given link can deliver inside the client's 30 s budget; it does not remove the ceiling. The ceiling itself (one 30 s budget for headers + body, identical retries forever, and rendering multi-tens-of-MB transcripts) is client-side and needs a follow-up in the web UI repository.

gen-docs: no documented user-facing surface changed (no new flag or config), so no doc update.

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.

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dbd7d51

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

Large live sessions could not open in `kimi web`: the transcript baseline
GET returned 200 but its body did not finish inside the web client's
30 s request budget, so the session rendered empty and retried forever.

- Add an onSend hook that gzips JSON/text responses >= 1 KiB when the
  request accepts gzip (synchronous gzip level 1, because Fastify
  re-sends an empty body if an onSend hook is still pending when an
  async handler that called reply.send resolves). The Remote Control
  tunnel strips Accept-Encoding, so tunnel traffic is unchanged.
- Cap the shell task outputTail in the live transcript projector
  (16 KiB bound, trimmed to 8 KiB via a task.upsert re-sync), matching
  the v1 projector; it was appended without limit and re-sent in full
  in every baseline response regardless of page_size.
@REtoolsx
REtoolsx force-pushed the issue-3881-fix-da37a9 branch from 4efefe6 to dbd7d51 Compare September 19, 2026 17:02
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.

web: transcript baseline GET times out at 30s on large live sessions (200 + parse phase), session renders empty and retries forever

1 participant