Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/export-log-limit.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 0 additions & 2 deletions docs/en/guides/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sessionId>` 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.
:::
Expand Down
2 changes: 0 additions & 2 deletions docs/zh/guides/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ kimi export <sessionId> -o ~/Desktop/my-session.zip

在 web UI 中,`/export` 会把当前会话下载为诊断 ZIP。压缩包包含持久化的会话数据、诊断日志,以及记录浏览器关键事件且大小有上限、只含元数据的 `logs/kimi-web.jsonl`;提示词正文、WebSocket 内容和 console 参数不会写入这份浏览器日志。这里的 web 命令与上面的 TUI `/export` 别名行为不同。

浏览器需要先把 ZIP 缓存在内存中再保存,因此 web 导出上限为 64 MiB。更大的会话请使用 `kimi export <sessionId>` 或 TUI 的 `/export-debug-zip`。

::: tip 提示
导出文件可能包含代码、命令输出和路径等敏感信息,分享前请先确认内容。
:::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ export interface ExportSessionResult {
export interface ExportSessionOptions {
readonly webLog?: string;
readonly signal?: AbortSignal;
readonly maxArchiveBytes?: number;
}

export interface ISessionExportService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export class SessionExportService implements ISessionExportService {
: undefined,
webLog: options.webLog,
signal: options.signal,
maxArchiveBytes: options.maxArchiveBytes,
});
}

Expand Down Expand Up @@ -185,7 +184,6 @@ export async function exportSessionDirectory(input: {
readonly desktopLogPath?: string | undefined;
readonly webLog?: string;
readonly signal?: AbortSignal;
readonly maxArchiveBytes?: number;
}): Promise<ExportSessionResult> {
input.signal?.throwIfAborted();
const sessionDir = input.summary.sessionDir;
Expand Down Expand Up @@ -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;
Expand Down
30 changes: 2 additions & 28 deletions packages/agent-core-v2/src/app/sessionExport/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -49,7 +49,6 @@ export async function writeExportZip(args: {
readonly sessionFiles: readonly SessionZipEntry[];
readonly extraEntries?: readonly ExtraZipEntry[];
readonly signal?: AbortSignal;
readonly maxArchiveBytes?: number;
}): Promise<readonly string[]> {
const unusedSources = new Set<ZipSource>([
...args.sessionFiles.flatMap((entry) => (typeof entry === 'string' ? [] : [entry.source])),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
14 changes: 0 additions & 14 deletions packages/kap-server/src/routes/sessionExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ import {
exportSessionRequestSchema,
} from '../protocol/rest-session';

const MAX_WEB_SESSION_EXPORT_BYTES = 64 * 1024 * 1024;

interface SessionExportRouteHost {
post(
path: string,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -132,7 +129,6 @@ export function registerSessionExportRoute(
{
webLog: req.body.web_log,
signal: exportAbort.signal,
maxArchiveBytes: MAX_WEB_SESSION_EXPORT_BYTES,
},
);
if (aborted) {
Expand Down Expand Up @@ -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(
Expand Down
Loading