From eb72aebeeb972b2fcc238d5650dd991a5580f96b Mon Sep 17 00:00:00 2001 From: Haozhe Date: Fri, 14 Aug 2026 11:48:56 +0800 Subject: [PATCH] fix(kap-server): remove the 64 MiB web session export limit (#2910) --- .changeset/export-log-limit.md | 5 ++++ docs/en/guides/sessions.md | 2 -- docs/zh/guides/sessions.md | 2 -- .../src/app/sessionExport/sessionExport.ts | 1 - .../app/sessionExport/sessionExportService.ts | 3 -- .../src/app/sessionExport/zip.ts | 30 ++----------------- .../app/sessionExport/sessionExport.test.ts | 17 ----------- .../kap-server/src/routes/sessionExport.ts | 14 --------- 8 files changed, 7 insertions(+), 67 deletions(-) create mode 100644 .changeset/export-log-limit.md diff --git a/.changeset/export-log-limit.md b/.changeset/export-log-limit.md new file mode 100644 index 000000000..019b54f94 --- /dev/null +++ b/.changeset/export-log-limit.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Remove the 64 MiB limit on web session exports, so large sessions no longer fail with a file-too-large error when downloaded from the web UI. diff --git a/docs/en/guides/sessions.md b/docs/en/guides/sessions.md index b63ee8d24..39aa2cb9e 100644 --- a/docs/en/guides/sessions.md +++ b/docs/en/guides/sessions.md @@ -110,8 +110,6 @@ You can also export from inside the TUI without leaving the interactive session: In the web UI, `/export` downloads the current session as a diagnostic ZIP. It includes the persisted session data, diagnostic logs, and a bounded metadata-only `logs/kimi-web.jsonl` record of key browser events. Prompt text, WebSocket payloads, and console arguments are not copied into this browser log. This web command differs from the TUI `/export` alias above. -The browser buffers the ZIP before saving it, so web exports are limited to 64 MiB. For a larger session, use `kimi export ` or the TUI `/export-debug-zip` command. - ::: tip Exported files may contain code, command output, and file paths that are sensitive. Review the content before sharing. ::: diff --git a/docs/zh/guides/sessions.md b/docs/zh/guides/sessions.md index fb979b077..6501d29a1 100644 --- a/docs/zh/guides/sessions.md +++ b/docs/zh/guides/sessions.md @@ -110,8 +110,6 @@ kimi export -o ~/Desktop/my-session.zip 在 web UI 中,`/export` 会把当前会话下载为诊断 ZIP。压缩包包含持久化的会话数据、诊断日志,以及记录浏览器关键事件且大小有上限、只含元数据的 `logs/kimi-web.jsonl`;提示词正文、WebSocket 内容和 console 参数不会写入这份浏览器日志。这里的 web 命令与上面的 TUI `/export` 别名行为不同。 -浏览器需要先把 ZIP 缓存在内存中再保存,因此 web 导出上限为 64 MiB。更大的会话请使用 `kimi export ` 或 TUI 的 `/export-debug-zip`。 - ::: tip 提示 导出文件可能包含代码、命令输出和路径等敏感信息,分享前请先确认内容。 ::: diff --git a/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts b/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts index 4b84bfe1f..ecf8e45fa 100644 --- a/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts +++ b/packages/agent-core-v2/src/app/sessionExport/sessionExport.ts @@ -58,7 +58,6 @@ export interface ExportSessionResult { export interface ExportSessionOptions { readonly webLog?: string; readonly signal?: AbortSignal; - readonly maxArchiveBytes?: number; } export interface ISessionExportService { diff --git a/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts b/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts index 561d3c558..73ee4c26f 100644 --- a/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts +++ b/packages/agent-core-v2/src/app/sessionExport/sessionExportService.ts @@ -99,7 +99,6 @@ export class SessionExportService implements ISessionExportService { : undefined, webLog: options.webLog, signal: options.signal, - maxArchiveBytes: options.maxArchiveBytes, }); } @@ -185,7 +184,6 @@ export async function exportSessionDirectory(input: { readonly desktopLogPath?: string | undefined; readonly webLog?: string; readonly signal?: AbortSignal; - readonly maxArchiveBytes?: number; }): Promise { input.signal?.throwIfAborted(); const sessionDir = input.summary.sessionDir; @@ -265,7 +263,6 @@ export async function exportSessionDirectory(input: { sessionFiles: selectedSessionFiles, extraEntries: extras, signal: input.signal, - maxArchiveBytes: input.maxArchiveBytes, }); sessionLogSourceTransferred = sessionLogSource !== undefined; globalSourceTransferred = globalSource !== undefined; diff --git a/packages/agent-core-v2/src/app/sessionExport/zip.ts b/packages/agent-core-v2/src/app/sessionExport/zip.ts index 7dbc97ae7..beb403eca 100644 --- a/packages/agent-core-v2/src/app/sessionExport/zip.ts +++ b/packages/agent-core-v2/src/app/sessionExport/zip.ts @@ -8,7 +8,7 @@ import { createWriteStream } from 'node:fs'; import { mkdir, mkdtemp, readdir, rename, rm, stat } from 'node:fs/promises'; -import { Readable, Transform } from 'node:stream'; +import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { dirname, join, relative, resolve } from 'pathe'; @@ -49,7 +49,6 @@ export async function writeExportZip(args: { readonly sessionFiles: readonly SessionZipEntry[]; readonly extraEntries?: readonly ExtraZipEntry[]; readonly signal?: AbortSignal; - readonly maxArchiveBytes?: number; }): Promise { const unusedSources = new Set([ ...args.sessionFiles.flatMap((entry) => (typeof entry === 'string' ? [] : [entry.source])), @@ -110,12 +109,7 @@ export async function writeExportZip(args: { args.signal?.addEventListener('abort', onAbort, { once: true }); const destination = createWriteStream(tempOutputPath, { flags: 'wx' }); - writing = - args.maxArchiveBytes === undefined - ? pipeline(output, destination, { signal: args.signal }) - : pipeline(output, createArchiveLimit(args.maxArchiveBytes), destination, { - signal: args.signal, - }); + writing = pipeline(output, destination, { signal: args.signal }); const activate = (source: ZipSource): Readable => { unusedSources.delete(source); @@ -263,26 +257,6 @@ function abortReason(signal: AbortSignal): Error { : new DOMException('The operation was aborted.', 'AbortError'); } -function createArchiveLimit(maxArchiveBytes: number): Transform { - let archiveBytes = 0; - return new Transform({ - transform(chunk: Buffer, _encoding, callback) { - archiveBytes += chunk.length; - if (archiveBytes > maxArchiveBytes) { - callback( - new Error2( - ErrorCodes.SESSION_EXPORT_TOO_LARGE, - `Session export exceeds the ${maxArchiveBytes} byte archive limit.`, - { details: { archiveBytes, maxArchiveBytes } }, - ), - ); - return; - } - callback(null, chunk); - }, - }); -} - async function findConflictingSource(args: { readonly outputPath: string; readonly sessionFiles: readonly SessionZipEntry[]; diff --git a/packages/agent-core-v2/test/app/sessionExport/sessionExport.test.ts b/packages/agent-core-v2/test/app/sessionExport/sessionExport.test.ts index 543c5c5bb..4eda5a157 100644 --- a/packages/agent-core-v2/test/app/sessionExport/sessionExport.test.ts +++ b/packages/agent-core-v2/test/app/sessionExport/sessionExport.test.ts @@ -710,23 +710,6 @@ describe('sessionExport', () => { expect((await readdir(tmp)).toSorted()).toEqual(['export.zip', 'safe-output', 'state.json']); }); - it('rejects with a coded error when compressed output exceeds the configured limit', async () => { - const tmp = await mkdtemp(join(tmpdir(), 'session-export-test-')); - - await expect( - writeExportZip({ - outputPath: join(tmp, 'too-large.zip'), - manifest: testManifest('ses_too_large'), - sessionDir: tmp, - sessionFiles: [], - maxArchiveBytes: 1, - }), - ).rejects.toMatchObject({ - code: 'session.export_too_large', - details: { maxArchiveBytes: 1 }, - }); - }); - it('throws a coded error when the session is unknown', async () => { const tmp = await mkdtemp(join(tmpdir(), 'session-export-test-')); ix = createTestServices(tmp, { diff --git a/packages/kap-server/src/routes/sessionExport.ts b/packages/kap-server/src/routes/sessionExport.ts index bc4754049..8fa1788a5 100644 --- a/packages/kap-server/src/routes/sessionExport.ts +++ b/packages/kap-server/src/routes/sessionExport.ts @@ -29,8 +29,6 @@ import { exportSessionRequestSchema, } from '../protocol/rest-session'; -const MAX_WEB_SESSION_EXPORT_BYTES = 64 * 1024 * 1024; - interface SessionExportRouteHost { post( path: string, @@ -64,7 +62,6 @@ export function registerSessionExportRoute( errors: { [ErrorCode.VALIDATION_FAILED]: {}, [ErrorCode.SESSION_NOT_FOUND]: {}, - [ErrorCode.FILE_TOO_LARGE]: {}, [ErrorCode.INTERNAL_ERROR]: {}, }, description: 'Export a session and diagnostic logs as a zip archive', @@ -132,7 +129,6 @@ export function registerSessionExportRoute( { webLog: req.body.web_log, signal: exportAbort.signal, - maxArchiveBytes: MAX_WEB_SESSION_EXPORT_BYTES, }, ); if (aborted) { @@ -200,16 +196,6 @@ function sendMappedError(reply: SessionExportReply, req: { id: string }, error: reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, error.message, requestId)); return; } - if (error.code === ErrorCodes.SESSION_EXPORT_TOO_LARGE) { - reply.send( - errEnvelope( - ErrorCode.FILE_TOO_LARGE, - 'session export exceeds the 64 MiB web limit', - requestId, - ), - ); - return; - } } requestLog(req)?.error({ err: error }, 'session export failed'); reply.send(