fix: eliminate racing saveData() calls during message restore

When onOpen() restored saved messages, each appendMessage() call
fired a fire-and-forget saveMessages() → saveData(). With N saved
messages, N overlapping saveData() writes raced against each other.
If a stale write landed last, the persisted data could be corrupted
or appear empty on next restart.

Fix: add _restoring flag. Skip saveMessages() inside appendMessage()
while restoring. Do a single clean save after the restore loop.
Also wraps the restore loop in try/catch so a rendering failure
in one message doesn't silently kill the entire restore.
This commit is contained in:
Helm 2026-07-10 13:26:57 -04:00
parent 0cebcf6084
commit fc5c9c01bf
2 changed files with 29 additions and 11 deletions

12
main.js

File diff suppressed because one or more lines are too long

View file

@ -35,6 +35,10 @@ export class ChatView extends ItemView {
private textarea: HTMLTextAreaElement | null = null; private textarea: HTMLTextAreaElement | null = null;
private sendButton: HTMLButtonElement | null = null; private sendButton: HTMLButtonElement | null = null;
/** True during initial message restore in onOpen() suppress
* saveMessages() calls to avoid N racing saveData() writes. */
private _restoring: boolean = false;
/** Context captured on chat focus avoids the timing bug where /** Context captured on chat focus avoids the timing bug where
* activeEditor is null by the time handleSend() runs. */ * activeEditor is null by the time handleSend() runs. */
private capturedNote: { private capturedNote: {
@ -176,13 +180,24 @@ export class ChatView extends ItemView {
this.statusBar = container.createDiv("chat-status-bar"); this.statusBar = container.createDiv("chat-status-bar");
this.updateStatus(this.chatState.model, null); this.updateStatus(this.chatState.model, null);
// Restore saved messages from previous session // Restore saved messages from previous session.
// Set _restoring flag so appendMessage skips fire-and-forget saves —
// we do a single save at the end to avoid N racing saveData() calls.
if (this.persistence) { if (this.persistence) {
const saved = this.persistence.loadMessages(); const saved = this.persistence.loadMessages();
if (saved.length > 0) { if (saved.length > 0) {
for (const msg of saved) { this._restoring = true;
await this.appendMessage(msg.role, msg.content); try {
for (const msg of saved) {
await this.appendMessage(msg.role, msg.content);
}
} catch (err) {
console.error("[Hermes] Failed to restore messages:", err);
} finally {
this._restoring = false;
} }
// Single save to persist the restored state cleanly
await this.persistence.saveMessages(this.chatState.messages);
} }
} }
} }
@ -228,8 +243,11 @@ export class ChatView extends ItemView {
timestamp: Date.now(), timestamp: Date.now(),
}; };
this.chatState.messages.push(msg); this.chatState.messages.push(msg);
// Fire-and-forget save — don't block rendering // Fire-and-forget save — don't block rendering.
this.persistence?.saveMessages(this.chatState.messages); // Suppressed during restore (too many racing saveData writes).
if (!this._restoring) {
this.persistence?.saveMessages(this.chatState.messages);
}
const wrapper = this.messagesContainer.createDiv("chat-message"); const wrapper = this.messagesContainer.createDiv("chat-message");
wrapper.addClass(`chat-message-${role}`); wrapper.addClass(`chat-message-${role}`);