How are turns inside sessions stored?

dev
5f3ab93wip: zen black
This post might have stale content, as dev is 1171 commits ahead.

how do turns inside sessions get stored? cause there's ai SDK's "UIMessage". wondering how stuff is stored.

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

Now I have the full picture. Let me summarize how turns/messages are stored:


##How Turns/Messages Are Stored in OpenCode

OpenCode has its own storage format, separate from AI SDK's UIMessage. Here's how it works:

###Storage Structure

Messages are stored as JSON files in a hierarchical file structure under ~/.opencode/storage/ (via the Global.Path.data directory):

storage/
├── session/{projectID}/{sessionID}.json     # Session metadata
├── message/{sessionID}/{messageID}.json     # Individual messages  
├── part/{messageID}/{partID}.json           # Message parts
├── session_diff/{sessionID}.json            # File diffs for session
└── todo/{sessionID}.json                    # TODOs for session

###Key Data Types

1. Session Info (Session.Info in index.ts:41-76):

  • Contains metadata like id, title, projectID, directory, timestamps, and optional share/revert info

2. Messages (MessageV2.Info in message-v2.ts:385-390): A discriminated union of two roles:

  • User Message (MessageV2.User):

    • role: "user"
    • agent, model (providerID + modelID), system prompt, tools, variant
    • Timestamp
  • Assistant Message (MessageV2.Assistant):

    • role: "assistant"
    • parentID (links to user message), providerID, modelID, agent
    • cost, tokens (input/output/reasoning/cache)
    • Optional error field
    • Optional summary, finish status

3. Parts (MessageV2.Part in message-v2.ts:330-344): Parts are stored separately from messages. A discriminated union including:

  • TextPart - actual text content
  • ReasoningPart - model's reasoning/thinking
  • FilePart - file attachments (images, etc.)
  • ToolPart - tool calls with state (pending/running/completed/error)
  • StepStartPart, StepFinishPart - step boundaries with cost/tokens
  • SnapshotPart, PatchPart - git snapshots
  • AgentPart, SubtaskPart, CompactionPart, RetryPart

###Conversion to AI SDK's UIMessage

The toModelMessages() function (message-v2.ts:437-606) converts OpenCode's format to AI SDK's UIMessage[]:

export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
  const result: UIMessage[] = []
  
  for (const msg of input) {
    if (msg.info.role === "user") {
      const userMessage: UIMessage = {
        id: msg.info.id,
        role: "user",
        parts: [],  // Built from TextPart, FilePart, etc.
      }
      // ...convert parts
    }
    
    if (msg.info.role === "assistant") {
      const assistantMessage: UIMessage = {
        id: msg.info.id,
        role: "assistant",
        parts: [],  // Built from TextPart, ToolPart, etc.
      }
      // ...convert parts including tool calls
    }
  }
  
  return convertToModelMessages(result, { tools })  // AI SDK function
}

###Storage Operations (via Storage namespace)

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

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

// Read messages for session  
for (const item of await Storage.list(["message", sessionID])) {
  const msg = await Storage.read<MessageV2.Info>(item)
}

// Read parts for message
for (const item of await Storage.list(["part", messageID])) {
  const part = await Storage.read<MessageV2.Part>(item)
}

###Key Takeaways

  1. Separation of concerns: Messages (Info) and their content (Part) are stored separately
  2. Rich part types: Much richer than UIMessage - includes tool states, reasoning, snapshots, costs
  3. Conversion layer: toModelMessages() handles the translation to AI SDK format when needed for LLM calls
  4. File-based storage: JSON files with file locking for concurrent access
  5. Parts are ordered by ID: IDs are ascending identifiers, sorted when retrieved

END OF POST

How are turns inside sessions stored? — anomalyco/opencode