fix: lighten session list payloads (#1538)
* lighten session list * fetch full session on open * sanitize session list * sanitize global sessions * preserve revert markers in session lists * fix: preserve session metadata in list sanitizers --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
1c274d0a56
commit
9685630436
@@ -2,13 +2,16 @@
|
||||
// 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). The UI never uses these fields but they
|
||||
// waste browser memory and
|
||||
// can crash tabs for large sessions.
|
||||
// (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"
|
||||
@@ -29,32 +32,134 @@ type SessionSummary = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type SessionRevert = {
|
||||
messageID?: string
|
||||
partID?: string
|
||||
snapshot?: string
|
||||
diff?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const getSessionListRevertMarker = (revert: unknown): Pick<SessionRevert, "messageID" | "partID"> | undefined => {
|
||||
if (!revert || typeof revert !== "object" || Array.isArray(revert)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const marker: Pick<SessionRevert, "messageID" | "partID"> = {}
|
||||
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 = <T extends DiffEntry>(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
|
||||
if (!summary?.diffs || !Array.isArray(summary.diffs)) return session
|
||||
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) => {
|
||||
if (d && (
|
||||
typeof d.before === "string"
|
||||
|| typeof d.after === "string"
|
||||
|| typeof d.from === "string"
|
||||
|| typeof d.to === "string"
|
||||
)) {
|
||||
const rest = { ...d }
|
||||
delete rest.before
|
||||
delete rest.after
|
||||
delete rest.from
|
||||
delete rest.to
|
||||
const nextDiff = stripDiffSnapshotFields(d, true)
|
||||
if (nextDiff !== d) {
|
||||
changed = true
|
||||
return rest
|
||||
}
|
||||
return d
|
||||
return nextDiff
|
||||
})
|
||||
|
||||
if (!changed) return session
|
||||
return { ...session, summary: { ...summary, diffs: stripped } } as Session
|
||||
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<string, unknown> = { ...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 */
|
||||
@@ -64,21 +169,11 @@ export function stripMessageDiffSnapshots(message: Message): Message {
|
||||
|
||||
let changed = false
|
||||
const stripped = summary.diffs.map((d) => {
|
||||
if (d && (
|
||||
typeof d.before === "string"
|
||||
|| typeof d.after === "string"
|
||||
|| typeof d.from === "string"
|
||||
|| typeof d.to === "string"
|
||||
)) {
|
||||
const rest = { ...d }
|
||||
delete rest.before
|
||||
delete rest.after
|
||||
delete rest.from
|
||||
delete rest.to
|
||||
const nextDiff = stripDiffSnapshotFields(d, false)
|
||||
if (nextDiff !== d) {
|
||||
changed = true
|
||||
return rest
|
||||
}
|
||||
return d
|
||||
return nextDiff
|
||||
})
|
||||
|
||||
if (!changed) return message
|
||||
|
||||
Reference in New Issue
Block a user