Skip to content

perf(cli,mcp): transcribe long recordings without loading the whole file into memory - #538

Merged
GeiserX merged 8 commits into
mainfrom
perf/streamed-upload
Nov 12, 2026
Merged

GeiserX merged 8 commits into
mainfrom
perf/streamed-upload

Conversation

@GeiserX

@GeiserX GeiserX commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Problem

akou transcribe and akou dictate FILE read the whole file into memory before they sent a byte. A 2 GB recording made the CLI grow by 2 GB or more. Swapping in Bun.file does not help: fetch with a FormData body builds the whole multipart body in memory first. We measured this on Bun 1.4.2 with a 256 MiB file. Transcribe grew by 537 MB and dictate by 806 MB. akou_transcribe in the MCP server has the same problem, although its comment said the file was never held in memory whole.

Fix

The client now sends every form as its own streamed multipart body. Each file part is read from disk as it goes out. The body still declares its Content-Length, so a server still refuses an upload over server.max_upload_mb before it reads any of it. If a file changes size during the upload, the request fails. It does not send a body of the wrong length. The CLI still checks that the file exists and can be read before it sends anything. The MCP comment now says what the code does.

There is one Bun detail. A Bun.file in a FormData keeps its full path as its name, and entries() drops the filename given to append. So the client names the file itself and sends only the last part of the path, never the folder.

Testing

  • New trap [UPL-1] in docs/TRAPS.md, tested in tests/upload-stream.e2e.test.ts.
  • The real CLI runs as a child process and sends a 256 MiB file to a fake server. We compare its peak memory with a 1 KiB upload. Before the fix, transcribe grew by 537 MB and dictate by 806 MB, so both fail the 96 MiB limit. After the fix, they grew by 32 MB and 27 MB.
  • Positive control: a child that sends the same FormData with plain fetch grows past the limit.
  • The streamed body goes through the server's own multipart reader, with and without a Content-Length. The fields, the file name and the file's sha256 all arrive intact.
  • check:static passes. The jobs, dictation, MCP, remote and multipart suites pass (180 tests).

…in memory

fetch with a FormData builds the whole multipart body in memory before it
sends a byte, even for a Bun.file part, so akou transcribe of a long
recording grew by more than the file. The client now sends every form as
a streamed multipart body with a computed Content-Length.
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between fd0dcb5 and 16df918.

Files selected for processing: 8

  • docs/TRAPS.md
  • src/main/cli/client.ts
  • src/main/cli/commands/dictate.ts
  • src/main/cli/commands/transcribe.ts
  • src/main/cli/upload-body.ts
  • src/main/mcp/server.ts
  • tests/fixtures/peak-rss.ts
  • tests/upload-stream.e2e.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

The parent's Subprocess.resourceUsage() gave the same peak for a 1 KiB and a
256 MiB FormData upload in the full suite on Linux, so the positive control saw
no growth there and the streamed-upload assertion could not fail. Each child now
writes its own peak on exit through a preload, and a peak below 8 MiB fails as
no measurement.
On Linux execve keeps the larger of the spawning process's resident size and
the child's in maxRSS, so in the full suite every child reported the test
runner's size and a 1 KiB and a 256 MiB upload read the same peak. The preload
now reads VmHWM from /proc/self/status there, and the growth assertions compare
the two peaks so a failure prints both.
@GeiserX
GeiserX merged commit a426ef5 into main Nov 12, 2026
30 checks passed
@GeiserX
GeiserX deleted the perf/streamed-upload branch November 12, 2026 14:44
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