// --------------------------------------------------------------------------- // Payload sanitization — strip oversized diff snapshot fields client-side. // // OpenCode session/message snapshots may carry large full-content diff fields // (legacy before/after or from/to). Revert snapshots and diff text can also // carry large file snapshots. The UI derives reverted-state behavior from the // lightweight messageID/partID markers, so these blob fields are intentionally // kept out of client stores. // // Applied at two points: // 1. Event reducer — session.created/session.updated events // 2. Message loading — fetchMessages response // 3. Session list loading — list responses should not populate stores with // detail-only revert/diff blobs // --------------------------------------------------------------------------- import type { Session, Message } from "@opencode-ai/sdk/v2/client" type DiffEntry = { file?: string status?: string additions?: number deletions?: number before?: string after?: string from?: string to?: string } type SessionSummary = { diffs?: DiffEntry[] [key: string]: unknown } type SessionRevert = { messageID?: string partID?: string snapshot?: string diff?: string [key: string]: unknown } const getSessionListRevertMarker = (revert: unknown): Pick | undefined => { if (!revert || typeof revert !== "object" || Array.isArray(revert)) { return undefined } const marker: Pick = {} const record = revert as SessionRevert if (typeof record.messageID === "string") { marker.messageID = record.messageID } if (typeof record.partID === "string") { marker.partID = record.partID } return Object.keys(marker).length > 0 ? marker : undefined } const hasSessionListRevertDetails = (revert: unknown): boolean => { if (revert === undefined) { return false } if (!revert || typeof revert !== "object" || Array.isArray(revert)) { return true } return Object.keys(revert).some((key) => key !== "messageID" && key !== "partID") } const stripDiffSnapshotFields = (diff: T, includePatch: boolean): T => { if (!diff || typeof diff !== "object") { return diff } const shouldStrip = typeof diff.before === "string" || typeof diff.after === "string" || typeof diff.from === "string" || typeof diff.to === "string" || (includePatch && "patch" in (diff as T & { patch?: unknown }) && typeof (diff as T & { patch?: unknown }).patch === "string") if (!shouldStrip) { return diff } const rest = { ...diff } as T & { patch?: string } delete rest.before delete rest.after delete rest.from delete rest.to if (includePatch) { delete rest.patch } return rest } /** Strip oversized snapshot fields from summary.diffs on a session object */ export function stripSessionDiffSnapshots(session: Session): Session { const summary = (session as { summary?: SessionSummary }).summary const revert = (session as { revert?: SessionRevert }).revert let nextSession: Session = session let changed = false if (revert && (typeof revert.snapshot === "string" || typeof revert.diff === "string")) { const nextRevert = { ...revert } delete nextRevert.snapshot delete nextRevert.diff nextSession = { ...nextSession, revert: nextRevert } as Session changed = true } if (!summary?.diffs || !Array.isArray(summary.diffs)) return nextSession const stripped = summary.diffs.map((d) => { const nextDiff = stripDiffSnapshotFields(d, true) if (nextDiff !== d) { changed = true } return nextDiff }) if (!changed) return nextSession return { ...nextSession, summary: { ...summary, diffs: stripped } } as Session } /** Strip detail-only fields from session list records before storing them. */ export function stripSessionListDetails(session: Session): Session { const record = session as Session & { summary?: SessionSummary revert?: unknown permission?: unknown } const shouldStrip = hasSessionListRevertDetails(record.revert) || Array.isArray(record.summary?.diffs) || "permission" in record if (!shouldStrip) { return session } const stripped = stripSessionDiffSnapshots(session) as typeof record const next: Record = { ...stripped } delete next.permission const revertMarker = getSessionListRevertMarker(stripped.revert) if (revertMarker) { next.revert = revertMarker } else { delete next.revert } const summary = stripped.summary if (summary && typeof summary === "object" && !Array.isArray(summary)) { const summaryWithoutDiffs = { ...summary } delete summaryWithoutDiffs.diffs next.summary = summaryWithoutDiffs } return next as unknown as Session } /** Strip oversized snapshot fields from summary.diffs on a message object */ export function stripMessageDiffSnapshots(message: Message): Message { const summary = (message as { summary?: SessionSummary }).summary if (!summary?.diffs || !Array.isArray(summary.diffs)) return message let changed = false const stripped = summary.diffs.map((d) => { const nextDiff = stripDiffSnapshotFields(d, false) if (nextDiff !== d) { changed = true } return nextDiff }) if (!changed) return message return { ...message, summary: { ...summary, diffs: stripped } } as Message }