How OpenCode's Session Summary Generation System Tracks Code Changes

OpenCode automatically generates session summaries by comparing snapshot markers from the start and end of assistant operations, tracking additions, deletions, and file changes in real-time.

OpenCode (anomalyco/opencode) implements a sophisticated session summary generation system that automatically records every code modification during AI-assisted development sessions. This system captures detailed metrics including line additions, deletions, and file-level changes by analyzing snapshot markers embedded in assistant message parts. The implementation resides primarily in packages/opencode/src/session/summary.ts and integrates with SQLite storage and an internal event bus for real-time UI updates.

What Is the Session Summary Generation System?

The session summary generation system is a three-stage pipeline that executes whenever the assistant completes an operation. It produces a structured record containing four key metrics: additions (new lines introduced), deletions (lines removed), files (count of modified files), and optional diffs (detailed per-file change objects).

The pipeline operates as follows:

  1. Collect all messages for the active session.
  2. Compute a diff between the earliest step-start snapshot and the latest step-finish snapshot.
  3. Persist the diff to storage and update the Session row in the SQLite database.

Changes are simultaneously broadcast on the internal event bus, enabling UI components like the session-diff view to refresh instantly without polling.

How the System Tracks Changes in OpenCode

Triggering the Summarization Process

The process begins when the summarize function in packages/opencode/src/session/summary.ts (lines 69-80) receives a session ID and the ID of the most recent user message:

export const summarize = fn(
  z.object({ sessionID: z.string(), messageID: z.string() }),
  async (input) => {
    const all = await Session.messages({ sessionID: input.sessionID })
    await Promise.all([
      summarizeSession({ sessionID: input.sessionID, messages: all }),
      summarizeMessage({ messageID: input.messageID, messages: all }),
    ])
  },
)

This function gathers all session messages and executes two parallel operations: summarizing the entire session and attaching specific diffs to the triggering message.

Computing Session-Level Diffs

The summarizeSession function (lines 83-98) aggregates statistics across all file changes:

async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
  const diffs = await computeDiff({ messages: input.messages })
  await Session.setSummary({
    sessionID: input.sessionID,
    summary: {
      additions: diffs.reduce((sum, x) => sum + x.additions, 0),
      deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
      files: diffs.length,
    },
  })
  await Storage.write(["session_diff", input.sessionID], diffs)
  Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
}

The system stores the detailed diff array under the key ["session_diff", sessionID] using the generic Storage layer backed by SQLite. It then publishes a Session.Event.Diff event to notify subscribers.

Attaching Diffs to Specific Messages

The summarizeMessage function (lines 100-112) filters messages to find the specific user message and its associated assistant responses:

async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
  const messages = input.messages.filter(
    (m) => m.info.id === input.messageID ||
           (m.info.role === "assistant" && m.info.parentID === input.messageID),
  )
  const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
  const userMsg = msgWithParts.info as MessageV2.User
  const diffs = await computeDiff({ messages })
  userMsg.summary = { ...userMsg.summary, diffs }
  await Session.updateMessage(userMsg)
}

This allows the UI to display exactly which changes resulted from a specific user prompt.

Analyzing Snapshot Markers

The core diff computation occurs in computeDiff (lines 35-60), which scans assistant message parts for snapshot markers:

export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
  let from: string | undefined
  let to: string | undefined

  for (const item of input.messages) {
    if (!from) {
      for (const part of item.parts) {
        if (part.type === "step-start" && part.snapshot) {
          from = part.snapshot
          break
        }
      }
    }

    for (const part of item.parts) {
      if (part.type === "step-finish" && part.snapshot) {
        to = part.snapshot
      }
    }
  }

  if (from && to) return Snapshot.diffFull(from, to)
  return []
}

The function identifies the earliest step-start snapshot (initial state) and the latest step-finish snapshot (final state), then delegates to Snapshot.diffFull to generate the actual file-level differences.

Normalizing File Paths for the UI

Stored diffs may contain escaped Git path strings (e.g., "src\\/main.ts"). The public diff API in packages/opencode/src/session/summary.ts (lines 14-33) cleans these paths:

export const diff = fn(
  z.object({
    sessionID: Identifier.schema("session"),
    messageID: Identifier.schema("message").optional(),
  }),
  async (input) => {
    const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
    const next = diffs.map((item) => {
      const file = unquoteGitPath(item.file)
      if (file === item.file) return item
      return { ...item, file }
    })
    const changed = next.some((item, i) => item.file !== diffs[i]?.file)
    if (changed) Storage.write(["session_diff", input.sessionID], next).catch(() => {})
    return next
  },
)

The unquoteGitPath utility decodes escaped octal sequences and quoted strings, ensuring the UI displays clean, readable file paths.

Database Schema and API Exposure

SQLite Storage Structure

The database schema defined in packages/opencode/src/session/session.sql.ts includes dedicated columns for summary metrics:

  • summary_additions – Total lines added
  • summary_deletions – Total lines removed
  • summary_files – Count of modified files
  • summary_diffs – Optional serialized diff objects

Retrieving Summary Data

The Session.getInfo method (exposed via the SessionInfo Zod schema in packages/opencode/src/session/index.ts) returns the summary block to callers:

const info = await Session.getInfo("s_12345")
console.log(info.summary)
// → { additions: 42, deletions: 7, files: 3, diffs: [...] }

Real-Time Updates via Event Bus

After each summarization, the system publishes Session.Event.Diff (defined in packages/opencode/src/session/index.ts, lines 76-82) to the internal event bus:

Bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })

This allows UI components to subscribe to change notifications and refresh the Changes tab instantly without polling the database.

Practical Implementation Examples

Summarize a Session Programmatically

import { SessionSummary } from "@opencode/session/summary"

await SessionSummary.summarize({
  sessionID: "s_12345",
  messageID: "m_67890",
})

Retrieve the Diff for UI Display

import { SessionSummary } from "@opencode/session/summary"

const diff = await SessionSummary.diff({
  sessionID: "s_12345",
})
// Returns: Array<{ file: string, additions: number, deletions: number, ... }>

Access High-Level Summary Statistics

import { Session } from "@opencode/session"

const info = await Session.getInfo("s_12345")
console.log(info.summary)
// → { additions: 42, deletions: 7, files: 3, diffs: [...] }

React to Changes in a React Frontend

import { useBus } from "@opencode/bus"
import { Session } from "@opencode/session"
import { useState, useEffect } from "react"

function SessionDiffView({ sessionID }: { sessionID: string }) {
  const [diff, setDiff] = useState([])

  useEffect(() => {
    const unsub = Bus.subscribe(Session.Event.Diff, ({ diff }) => {
      if (diff.sessionID === sessionID) setDiff(diff.diff)
    })
    return () => unsub()
  }, [sessionID])

  return <DiffTable rows={diff} />
}

Key Source Files

File Role
[packages/opencode/src/session/summary.ts](https://github.com/anomalyco/opencode/blob/dev/dev/packages/opencode/src/session/summary.ts) Core summarizer implementing summarize, summarizeSession, summarizeMessage, computeDiff, and diff
[packages/opencode/src/session/index.ts](https://github.com/anomalyco/opencode/blob/dev/dev/packages/opencode/src/session/index.ts) Session model, database mapping, SessionInfo schema, and Session.Event.Diff definition
[packages/opencode/src/session/session.sql.ts](https://github.com/anomalyco/opencode/blob/dev/dev/packages/opencode/src/session/session.sql.ts) SQLite table schema with summary_additions, summary_deletions, summary_files, and summary_diffs columns
[packages/opencode/src/snapshot/index.ts](https://github.com/anomalyco/opencode/blob/dev/dev/packages/opencode/src/snapshot/index.ts) Snapshot.diffFull implementation that computes actual file-level differences between snapshots
[packages/opencode/src/storage/storage.ts](https://github.com/anomalyco/opencode/blob/dev/dev/packages/opencode/src/storage/storage.ts) Generic key/value storage layer used to persist diff arrays under ["session_diff", sessionID] keys

Summary

  • OpenCode's session summary generation system automatically records code changes by comparing step-start and step-finish snapshots stored in assistant message parts.
  • The system calculates additions, deletions, and file counts by delegating to Snapshot.diffFull in packages/opencode/src/snapshot/index.ts.
  • Detailed diffs persist in SQLite via the Storage layer under the key ["session_diff", sessionID], while high-level statistics write to summary_additions, summary_deletions, and summary_files columns.
  • Real-time UI updates occur through Session.Event.Diff events published on the internal bus, enabling live refresh of the Changes tab without polling.

Frequently Asked Questions

How does OpenCode detect which files changed during a session?

OpenCode detects file changes by scanning assistant message parts for step-start and step-finish snapshot markers in the computeDiff function within packages/opencode/src/session/summary.ts. The system identifies the earliest step-start snapshot (representing the initial state) and the latest step-finish snapshot (representing the final state), then delegates to Snapshot.diffFull to generate an array of FileDiff objects containing the specific files modified, lines added, and lines deleted.

Where are session summaries stored in OpenCode?

Session summaries are stored in two locations within the SQLite database managed by the Storage layer. High-level statistics (additions, deletions, file count) are stored in the Session table columns defined in packages/opencode/src/session/session.sql.ts: summary_additions, summary_deletions, summary_files, and summary_diffs. Detailed per-file diff objects are stored as JSON in the generic key-value store under the composite key ["session_diff", sessionID] via Storage.write in packages/opencode/src/storage/storage.ts.

Can I access historical session diffs programmatically?

Yes, historical session diffs are accessible through the public SessionSummary.diff API exported from packages/opencode/src/session/summary.ts. You can retrieve the complete diff array for any session by calling SessionSummary.diff({ sessionID: "s_12345" }), which returns an array of FileDiff objects containing file paths, addition counts, and deletion counts. Additionally, Session.getInfo(sessionID) returns high-level summary statistics suitable for displaying change counts in dashboards or CLI outputs.

How does the session summary generation system handle file renames?

The session summary generation system handles file renames through the unquoteGitPath utility function invoked within the diff function in packages/opencode/src/session/summary.ts. When diffs are retrieved from storage, file paths may contain escaped Git path strings (such as "src\\/main.ts" with escaped slashes or octal sequences). The unquoteGitPath function decodes these escaped sequences and quoted strings, returning clean file paths for UI display. If any paths are normalized during this process, the corrected diff array is written back to storage to ensure consistency.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →