How are messages and parts stored in opencode?

dev
8a216a6fix(app): normalize path separators for session diff filtering on Windows (#10291)
This post might have stale content, as dev is 1120 commits ahead.

how do they store messages and parts? separately? is message -> UIMessage from the ai sdk?

Avatar of anthropic/claude-opus-4.5
Claude Opus 4.5
commented

Now I have a clear picture. Let me summarize what I found:

##Summary: Messages and Parts Storage

Yes, messages and parts are stored separately.

###Storage Structure

The storage uses a file-based system with paths like:

  • Messages: ["message", sessionID, messageID] → stored as storage/message/{sessionID}/{messageID}.json
  • Parts: ["part", messageID, partID] → stored as storage/part/{messageID}/{partID}.json

###Key Types

From packages/opencode/src/session/message-v2.ts:

  1. MessageV2.Info - The message metadata (discriminated union of User and Assistant):

    • User message: role, time, agent, model, tools, etc.
    • Assistant message: role, time, tokens, cost, error, modelID, providerID, etc.
  2. MessageV2.Part - A discriminated union of many part types:

    • TextPart, ReasoningPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart, AgentPart, RetryPart, CompactionPart, SubtaskPart
  3. MessageV2.WithParts - Combines both:

    export const WithParts = z.object({
      info: Info,
      parts: z.array(Part),
    })
    

###Relation to AI SDK's UIMessage

Yes, MessageV2.Info is converted to UIMessage from the AI SDK, but it's not a direct mapping.

In toModelMessages() (line 436), they:

  1. Take an array of WithParts[]
  2. Build UIMessage[] objects (from import { type UIMessage } from "ai")
  3. Transform their custom part types into the AI SDK's expected format
  4. Call convertToModelMessages() from the AI SDK

So their internal MessageInfo structure is their own schema, which gets converted to UIMessage when interfacing with the AI SDK for model calls.

###Storage Operations

From packages/opencode/src/session/index.ts:

// Write a message
await Storage.write(["message", msg.sessionID, msg.id], msg)

// Write a part  
await Storage.write(["part", part.messageID, part.id], part)

// Remove a part
await Storage.remove(["part", input.messageID, input.partID])

Parts are retrieved by listing all files in the part directory for a message:

export const parts = fn(Identifier.schema("message"), async (messageID) => {
  const result = [] as MessageV2.Part[]
  for (const item of await Storage.list(["part", messageID])) {
    const read = await Storage.read<MessageV2.Part>(item)
    result.push(read)
  }
  result.sort((a, b) => (a.id > b.id ? 1 : -1))
  return result
})

END OF POST

How are messages and parts stored in opencode? — anomalyco/opencode