Skip to content

feat: offload large base64 media payloads to external blob files - #117

Merged
kermanx merged 17 commits into
mainfrom
feat/wire-blob-offloading
May 28, 2026
Merged

feat: offload large base64 media payloads to external blob files#117
kermanx merged 17 commits into
mainfrom
feat/wire-blob-offloading

Conversation

@kermanx

@kermanx kermanx commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

wire.jsonl grows very large when conversations contain images, audio, or video embedded as base64 data URIs. During session replay, the entire wire must be traversed, causing high memory pressure as every base64 payload is parsed into memory even when the media is not immediately needed.

What changed

  • Introduce "blobref" concept in a new blobref.ts module.
    • On write: large data URIs (>4KB threshold) are extracted into external blob files (), and the wire record stores a lightweight placeholder.
    • On read: blobrefs are rehydrated back to full data URIs only when messages are about to be sent to the LLM provider.
  • transparently offloads on write; rehydrates before .
  • Blobref format mirrors data URI layout () for consistency.
  • Blob files are named by SHA-256 of the base64 payload, providing natural deduplication.
  • Missing blobs degrade to a text placeholder () so the LLM provider does not receive an invalid media block.
  • Export (zip) naturally includes blob directories via recursive file collection.

Key design decisions:

  • Offload scans only known record types (, , , tool.result) to avoid touching unrelated strings.
  • Within a ContentPart, media URLs are found by structural pattern () rather than hard-coding field names, so new media types are supported automatically.

Checklist

  • I have read the CONTRIBUTING document.
  • I have explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill.

Wire.jsonl grows very large when conversations contain images, audio,
or video embedded as data: URIs. This change extracts base64 payloads
exceeding a threshold (default 4KB) into per-agent blob files stored
alongside wire.jsonl, replacing the inline data with a lightweight
blobref URL.

- New blobref.ts handles offload / rehydrate / missing-blob downgrade.
- Blobref format mirrors data URI: blobref:<mime>;<hash>.
- FileSystemAgentRecordPersistence transparently offloads on write.
- KosongLLM rehydrates blobrefs back to data URIs before calling the
  LLM provider, so the rest of the stack is unaware of blobrefs.
- Export (zip) naturally includes blob directories via recursive file
collection, no extra changes needed.

Key design decisions:
- Offload scans only known record types (turn.prompt, turn.steer,
  context.append_message, context.append_loop_event tool.result) to
  avoid accidentally touching unrelated strings.
- Within a ContentPart, media URLs are found by structural pattern
  ({ xxxUrl: { url } }) rather than hard-coding field names, so new
  media types are supported automatically.
- Blob files are named by SHA-256 of the base64 payload, providing
  natural deduplication.
- Missing blobs degrade to a text placeholder so the LLM provider
  does not receive an invalid media block.
@changeset-bot

changeset-bot Bot commented May 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d776679

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

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/agent-core Patch
@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

@pkg-pr-new

pkg-pr-new Bot commented May 27, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@d776679
npx https://pkg.pr.new/@moonshot-ai/kimi-code@d776679

commit: d776679

@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: 3cd02dffc6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/agent-core/src/agent/records/blobref.ts Outdated
Comment thread packages/agent-core/src/agent/records/blobref.ts Outdated
Comment thread packages/agent-core/src/agent/records/persistence.ts Outdated
@kermanx

kermanx commented May 27, 2026

Copy link
Copy Markdown
Collaborator Author

@codex

@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: b4d1dfc607

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +92 to +94
if (this.blobStore !== undefined) {
messages = await this.blobStore.rehydrateMessages(params.messages);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rehydrate blobrefs before compaction requests

When a session with offloaded media is resumed, replay restores context.append_message / tool-result content with blobref: URLs in ContextMemory; only normal turn requests pass through this new KosongLLM.chat rehydration path. Full compaction still builds projected history and calls this.agent.generate directly in packages/agent-core/src/agent/compaction/full.ts, so manual/auto compaction after such a resume sends blobref: URLs to the provider instead of the original data URIs, causing invalid media input or summaries that silently omit the media. Rehydrate the compaction messages too, or centralize this at the lower generate seam used by both paths.

Useful? React with 👍 / 👎.

Comment on lines +207 to +208
this.setCache(hash, base64Payload);
return `${BLOBREF_PROTOCOL}${mimeType};${hash}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bump the wire protocol for blobrefs

This writes a new on-disk representation into wire.jsonl, but the commit leaves AGENT_WIRE_PROTOCOL_VERSION at 1.2 (checked packages/agent-core/src/agent/records/migration/index.ts). If a session written by this version is later resumed by an older 1.2 reader, it will treat blobref: as a normal media URL and send invalid media to the provider without any version warning or migration gate. Please bump the wire version and add the corresponding migration/compatibility handling when introducing blobrefs.

Useful? React with 👍 / 👎.

Comment on lines +137 to +139
this.blobStore = config.homedir
? new BlobStore({ blobsDir: join(config.homedir, 'blobs') })
: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wire injected blob stores into the agent

When callers use the public persistence injection path with new FileSystemAgentRecordPersistence(..., { blobStore }), this constructor still creates the rehydrating store only from homedir. A standalone or test Agent with custom persistence and no matching homedir can therefore write/replay blobref: records but then send those unresolved URLs on normal LLM turns because KosongLLM receives undefined (or the wrong blob directory). Expose/pass the persistence blob store into Agent instead of deriving it only here.

Useful? React with 👍 / 👎.

@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: 7639023b2a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +220 to +223
while (this.currentCacheSize + size > this.maxCacheSize && this.cache.size > 0) {
this.evictLRU();
}
this.currentCacheSize += size;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip caching blobs larger than the cache cap

When a single media blob is larger than maxCacheSize, this loop evicts every existing entry but then still inserts the oversized payload and increments currentCacheSize past the cap. This can happen with existing ReadMediaFileTool inputs up to 100MB (about 133MB base64), so the default 50MB cache can retain a huge string after offload/rehydration and defeat the memory-pressure goal; avoid caching entries whose size exceeds the cap.

Useful? React with 👍 / 👎.

Comment thread packages/agent-core/src/agent/records/index.ts
Comment thread packages/agent-core/src/agent/records/blobref.ts

@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: 1edbeeab29

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +41 to +46
content = await readFile(blobPath);
} catch {
return c.json({ error: 'blob not found', code: 'NOT_FOUND' }, 404);
}
const mimeType = c.req.query('mime') ?? 'application/octet-stream';
return new Response(content, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Decode blob payloads before serving media

For offloaded media, this route sends the blob file bytes directly with the requested media content type, but BlobStore.writeBlob stores only the base64 payload string in the file. In vis, an image URL like /api/sessions/.../blobs/<hash>?mime=image/png will therefore return ASCII bytes such as iVBOR... as image/png instead of the decoded PNG bytes, so large offloaded images/audio/video still fail to preview. Decode the file contents from base64 before constructing the response, or return a data URL instead.

Useful? React with 👍 / 👎.

@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: de813451ec

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

agentId: string,
): void {
for (const entry of entries) {
rehydrateRecord(entry.data as Record<string, unknown>, sessionId, agentId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve raw wire entries when resolving blobrefs

When readAgentWire reads current 1.3 records, it returns the same object for entry.data and entry.raw whenever no migration applies (apps/vis/server/src/lib/wire-reader.ts:87-101). This in-place mutation therefore also rewrites entry.raw, so the wire detail's “copy raw” / “as written on disk” view shows /api/sessions/... URLs instead of the actual blobref: value from wire.jsonl for current sessions with offloaded media. Clone entry.data before resolving blobrefs, or have the reader return a distinct migrated object.

Useful? React with 👍 / 👎.

Comment thread apps/vis/server/src/routes/blobs.ts Outdated
Comment on lines +32 to +38
const blobPath = join(
detail.sessionDir,
'agents',
agentId,
'blobs',
hash,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the recorded agent homedir for blobs

For sessions where an agent's recorded homedir is not exactly <sessionDir>/agents/<id> (for example callers can pass config.homedir through Session.createAgent, and imported/remapped state preserves meta.homedir), the agent writes offloaded media to homedir/blobs, but this route always reads under detail.sessionDir/agents/<id>/blobs. In that context the wire/context endpoints resolve blobrefs to this new route, but every preview 404s even though detail.agents already contains the correct agent.homedir; build the path from that recorded homedir instead.

Useful? React with 👍 / 👎.

const mimeType = rest.slice(0, semiIdx);
const hash = rest.slice(semiIdx + 1);
if (hash.length === 0) return url;
return `/api/sessions/${encodeURIComponent(sessionId)}/blobs/${encodeURIComponent(hash)}?agent=${encodeURIComponent(agentId)}&mime=${encodeURIComponent(mimeType)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return URLs the image preview treats as supported

Fresh evidence after the vis resolver was added: ImagePreview still only renders URLs starting with data:image/ or http(s):// (apps/vis/web/src/components/shared/ImagePreview.tsx:19), but this resolver returns a root-relative /api/... URL. For any offloaded image shown in the context or wire views, the UI therefore stops at the “unsupported scheme” fallback and never requests the blob route; return an absolute URL or update the preview predicate to accept same-origin paths.

Useful? React with 👍 / 👎.

@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: 3eac4c4e82

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/agent-core/src/agent/records/index.ts
Comment thread apps/vis/server/src/lib/blob-resolver.ts

const blobFiles = await readdir(blobsDir);
expect(blobFiles).toHaveLength(1);
expect(await readFile(join(blobsDir, blobFiles[0]!), 'utf8')).toBe(payload);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare blob bytes as base64

This new assertion expects the blob file to contain the original base64 text, but BlobStore.writeBlob decodes that string with Buffer.from(base64Payload, 'base64') before writing. With the test payload of 'X'.repeat(5000), reading the file as UTF-8 will not equal payload, so the added persistence test fails even though offloading wrote the decoded bytes correctly; compare readFile(...).toString('base64') or the expected decoded buffer instead.

Useful? React with 👍 / 👎.

kermanx added 3 commits May 28, 2026 15:16
BlobStore.offload now returns a new AgentRecord with offloaded URLs
instead of mutating the input record's ContentPart objects. Tool result
parts are shared by reference between persistence records and the
in-memory context history; mutating them during the async flush could
let blobref: URLs leak into the next LLM request body.

drainBatch uses the returned record for JSON serialization. Adds tests
for the non-mutating contract and for the previously uncovered
context.append_loop_event/tool.result path. Also fixes an unrelated
broken assertion in persistence.test.ts that compared a binary blob
file against the base64 string.
Fold the read-through cache changeset into the offloading one — both
ship together on this branch under the same feature.
@kermanx
kermanx merged commit a6d379b into main May 28, 2026
6 checks passed
@kermanx
kermanx deleted the feat/wire-blob-offloading branch May 28, 2026 08:47
@github-actions github-actions Bot mentioned this pull request May 28, 2026
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