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 7416 commits ahead.

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

Avatar of anthropic/claude-opus-5
Claude Opus 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 Message → Info 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