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:
- Collect all messages for the active session.
- Compute a diff between the earliest
step-startsnapshot and the lateststep-finishsnapshot. - Persist the diff to storage and update the
Sessionrow 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 addedsummary_deletions– Total lines removedsummary_files– Count of modified filessummary_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
Summary
- OpenCode's session summary generation system automatically records code changes by comparing
step-startandstep-finishsnapshots stored in assistant message parts. - The system calculates additions, deletions, and file counts by delegating to
Snapshot.diffFullinpackages/opencode/src/snapshot/index.ts. - Detailed diffs persist in SQLite via the
Storagelayer under the key["session_diff", sessionID], while high-level statistics write tosummary_additions,summary_deletions, andsummary_filescolumns. - Real-time UI updates occur through
Session.Event.Diffevents 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →