Skip to content

Commit ca4a24a

Browse files
authored
sessions: be overly verbose about errors that happen during serialization (#298350)
Not sure the error is in here, but if it is, this should help.
1 parent 6c24652 commit ca4a24a

File tree

2 files changed

+81
-4
lines changed

2 files changed

+81
-4
lines changed

src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { joinPath } from '../../../../../base/common/resources.js';
1313
import { URI } from '../../../../../base/common/uri.js';
1414
import { localize } from '../../../../../nls.js';
1515
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
16+
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
1617
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
1718
import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../../platform/files/common/files.js';
1819
import { ILogService } from '../../../../../platform/log/common/log.js';
20+
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
1921
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
2022
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
2123
import { IUserDataProfilesService } from '../../../../../platform/userDataProfile/common/userDataProfile.js';
@@ -57,6 +59,8 @@ export class ChatSessionStore extends Disposable {
5759
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
5860
@IConfigurationService private readonly configurationService: IConfigurationService,
5961
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
62+
@IDialogService private readonly dialogService: IDialogService,
63+
@IOpenerService private readonly openerService: IOpenerService,
6064
) {
6165
super();
6266

@@ -339,6 +343,8 @@ export class ChatSessionStore extends Disposable {
339343
}
340344
}
341345

346+
private _didReportIssue = false;
347+
342348
private async writeSession(session: ChatModel | ISerializableChatData): Promise<void> {
343349
try {
344350
const index = this.internalGetIndex();
@@ -349,7 +355,32 @@ export class ChatSessionStore extends Disposable {
349355
session.dataSerializer = new ChatSessionOperationLog();
350356
}
351357

352-
const { op, data } = session.dataSerializer.write(session);
358+
let op: 'append' | 'replace';
359+
let data: VSBuffer;
360+
try {
361+
({ op, data } = session.dataSerializer.write(session));
362+
} catch (e) {
363+
// This is a big of an ugly prompt, but there is _something_ going on with
364+
// missing sessions. Unfortunately it's hard to root cause because users would
365+
// not notice an error until they reload the window, at which point any error
366+
// is gone. Throw a very verbose dialog here so we can get some quality
367+
// bug reports, if the issue is indeed in the serialized.
368+
// todo@connor4312: remove after a little bit
369+
if (!this._didReportIssue) {
370+
this._didReportIssue = true;
371+
this.dialogService.prompt({
372+
custom: true, // so text is copyable
373+
title: localize('chatSessionStore.serializationError', 'Error saving chat session'),
374+
message: localize('chatSessionStore.writeError', 'Error serializing chat session for storage. The session will be lost if the window is closed. Please report this issue to the VS Code team:\n\n{0}', e.stack || toErrorMessage(e)),
375+
buttons: [
376+
{ label: localize('reportIssue', 'Report Issue'), run: () => this.openerService.open('https://github.com/microsoft/vscode/issues/new?template=bug_report.md') }
377+
]
378+
});
379+
}
380+
381+
throw e;
382+
}
383+
353384
if (data.byteLength > 0) {
354385
await this.fileService.writeFile(storageLocation.log, data, { append: op === 'append' });
355386
}

src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ import { assertNever } from '../../../../../base/common/assert.js';
77
import { VSBuffer } from '../../../../../base/common/buffer.js';
88
import { isUndefinedOrNull } from '../../../../../base/common/types.js';
99

10+
/**
11+
* Updates an error's message and stack trace with a prefix. In V8 the stack
12+
* string starts with "ErrorName: message\n at …", so we rebuild the header
13+
* after mutating the message.
14+
*/
15+
function prefixError(e: Error, prefix: string): void {
16+
e.message = prefix + e.message;
17+
if (e.stack) {
18+
const nlIdx = e.stack.indexOf('\n');
19+
e.stack = nlIdx !== -1
20+
? `${e.name}: ${e.message}${e.stack.slice(nlIdx)}`
21+
: `${e.name}: ${e.message}`;
22+
}
23+
}
24+
25+
/**
26+
* Prepends a path segment to an error as it unwinds through nested extract
27+
* calls. Each level adds its segment so the final message reads e.g.
28+
* `.responses[2].content: Cannot read property 'x' of undefined`.
29+
*/
30+
function rethrowWithPathSegment(e: unknown, segment: string | number): never {
31+
if (e instanceof Error) {
32+
const part = typeof segment === 'number' ? `[${segment}]` : `.${segment}`;
33+
const needsSep = !e.message.startsWith('[') && !e.message.startsWith('.');
34+
prefixError(e, part + (needsSep ? ': ' : ''));
35+
}
36+
throw e;
37+
}
1038

1139
/** IMPORTANT: `Key` comes first. Then we should sort in order of least->most expensive to diff */
1240
const enum TransformKind {
@@ -96,7 +124,13 @@ export function array<T, R>(schema: TransformObject<T, R> | TransformValue<T, R>
96124
return {
97125
kind: TransformKind.Array,
98126
itemSchema: schema,
99-
extract: from => from?.map(item => schema.extract(item)),
127+
extract: from => from?.map((item, i) => {
128+
try {
129+
return schema.extract(item);
130+
} catch (e) {
131+
rethrowWithPathSegment(e, i);
132+
}
133+
}),
100134
};
101135
}
102136

@@ -124,7 +158,11 @@ export function object<T, R extends object>(schema: Schema<T, R>, options?: Obje
124158

125159
const result: Record<string, unknown> = Object.create(null);
126160
for (const [key, transform] of entries) {
127-
result[key] = transform.extract(from);
161+
try {
162+
result[key] = transform.extract(from);
163+
} catch (e) {
164+
rethrowWithPathSegment(e, key);
165+
}
128166
}
129167
return result as R;
130168
},
@@ -278,7 +316,15 @@ export class ObjectMutationLog<TFrom, TTo> {
278316
// Generate diff entries
279317
const entries: Entry[] = [];
280318
const path: ObjectPath = [];
281-
this._diff(this._transform, path, this._previous, currentValue, entries);
319+
try {
320+
this._diff(this._transform, path, this._previous, currentValue, entries);
321+
} catch (e) {
322+
if (e instanceof Error) {
323+
const pathStr = path.map(s => typeof s === 'number' ? `[${s}]` : `.${s}`).join('') || '<root>';
324+
prefixError(e, `error diffing at ${pathStr}: `);
325+
}
326+
throw e;
327+
}
282328

283329
if (entries.length === 0) {
284330
// No changes

0 commit comments

Comments
 (0)