MusePi

Blob and artifact storage architecture

English 中文

This document describes how coding-agent stores large/binary payloads outside session JSONL, how truncated tool output is persisted, and how internal URLs (artifact://, agent://) resolve back to stored data.

Why two storage systems exist

The runtime uses two different persistence mechanisms for different data shapes:

They are intentionally separate:

Storage boundaries and on-disk layout

Blob store boundary (global)

SessionManager constructs BlobStore(getBlobsDir()), so blob files live in a shared global blob directory, not in a session folder.

Blob file naming:

Implications:

Artifact boundary (session-local)

ArtifactManager derives artifact directory from session file path:

Artifact types share this directory:

Subagents can adopt the parent ArtifactManager; in that case parent and subagent tree share one artifact directory and numeric artifact ID space.

ID and name allocation schemes

Blob IDs: content hash

BlobStore.put() / putSync() computes SHA-256 over the bytes it is given and returns:

No session-local counter is used.

Artifact IDs: session-local monotonic integer

ArtifactManager scans existing *.log artifact files on first directory-backed allocation to find max existing numeric ID and sets nextId = max + 1.

Allocation behavior:

If the artifact directory is missing, scanning yields an empty list and allocation starts from 0.

Non-persistent sessions without an adopted manager can store saveArtifact(...) content in memory under numeric IDs, but artifact:// resolution is file-backed through registered artifact directories.

Agent output IDs (agent://)

AgentOutputManager allocates IDs for subagent outputs from the requested name, used verbatim the first time and suffixed (-2, -3, …) only when the same name repeats (e.g. Anna, Anna-2). Nested outputs are grouped under the parent prefix (e.g. Parent.Child). It scans existing .md files on initialization so a resumed session never reuses a name that would clobber a prior output.

Persistence dataflow

1) Session entry persistence rewrite path

Before a session entry is written — incremental append (#appendToSessionFile) or a full-file rewrite (#rewriteSynchronously / #rewriteAtomically) — SessionManager serializes it through #lineFor(), which runs prepareEntryForPersistence() over the truncation pipeline.

Key behaviors:

  1. Large string truncation: oversized strings are cut and suffixed with "[Session persistence truncated large content]"; signature fields (thinkingSignature, thoughtSignature, textSignature) are cleared instead of truncated.
  2. Transient field stripping: partialJson and jsonlEvents are removed from persisted entries.
  3. Image externalization to blobs:
    • image blocks in content arrays are externalized when data is not already a blob ref and base64 length is at least threshold (BLOB_EXTERNALIZE_THRESHOLD = 1024),
    • provider-style image_url data URLs are externalized when they start with data:image/ and contain ;base64,,
    • image block data is stored as decoded binary bytes,
    • provider data URLs are stored as the original UTF-8 data URL string,
    • persisted values are replaced with blob:sha256:<hash>.

This keeps session JSONL compact while preserving recoverability.

2) Session load rehydration path

When opening a session (setSessionFile), after migrations, SessionManager runs resolveBlobRefsInEntries().

For message/custom-message image blocks with blob:sha256:<hash> and for persisted provider image_url fields with blob refs:

If a blob is missing:

3) Tool output spill/truncation path

OutputSink powers streaming output in bash/python/ssh and related executors.

Behavior:

  1. Every chunk is sanitized with sanitizeWithOptionalSixelPassthrough(..., sanitizeText) and appended to in-memory accounting.
  2. Optional live onChunk receives sanitized pre-column-cap chunks, throttled if configured.
  3. A per-line column cap can drop bytes from long lines in the LLM-facing buffer; when this happens, artifact mirroring starts so the on-disk file keeps the full sanitized stream.
  4. When the in-memory tail buffer would exceed spill threshold (DEFAULT_MAX_BYTES, 50KB), sink marks output truncated and starts artifact mirroring if an artifact path is available.
  5. If a file sink is opened, it first writes the current buffer, then all queued/subsequent sanitized chunks.
  6. In-memory buffer is trimmed to a tail window, or to head + elision marker + tail when head retention is configured.
  7. dump() returns summary including artifactId only when file sink creation succeeded.

Practical effect:

If file sink creation fails (I/O error, missing path, etc.), sink falls back to in-memory truncation only; full output is not persisted.

URL access model

blob: references

blob:sha256:<hash> is a persistence reference inside session entry payloads, not an internal URL scheme handled by the router. Resolution is done by SessionManager during session load.

artifact://<id>

Handled by ArtifactProtocolHandler over registered active session artifact directories:

Failure behavior:

agent://<id>

Handled by AgentProtocolHandler over registered active session artifact directories and <artifactsDir>/<id>.md:

Failure behavior:

Read tool integration:

Resume, fork, and move semantics

Resume

Fork

SessionManager.fork() creates a new session file with new session ID and parentSession link, then returns old/new file paths. Artifact copying is handled by AgentSession.fork():

ID implications after fork:

Blob implications after fork:

Move to new cwd

SessionManager.moveTo() renames both session file and artifact directory to the new default session directory, with rollback logic if a later step fails. This preserves artifact identity while relocating session scope.

Failure handling and fallback paths

Case Behavior
Blob file missing during image-block rehydration Warn and keep blob:sha256: ref string in memory
Blob file missing during provider image_url rehydration Warn and keep blob:sha256: ref string in memory
Blob read ENOENT via BlobStore.get Returns null
Artifact directory missing (ArtifactManager.listFiles) Returns empty list (allocation can start fresh)
No registered artifact dirs (artifact://) Throws No session - artifacts unavailable
No registered artifact dirs (agent://) Throws No session - agent outputs unavailable
Registered artifact dirs missing on disk Throws explicit No artifacts directory found
Artifact ID not found Throws with available IDs listing
OutputSink artifact writer init fails Continues with bounded in-memory output only
Non-persistent saveArtifact Stores text in SessionManager memory map; not file-backed URL data

Binary blob externalization vs text-output artifacts

The two systems intersect only indirectly: both reduce session JSONL bloat, but they have different identity, lifetime, and retrieval paths.

Implementation files