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 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
* activeEditor is null by the time handleSend() runs. */
private capturedNote: {
@ -176,13 +180,24 @@ export class ChatView extends ItemView {
this.statusBar = container.createDiv("chat-status-bar");
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) {
const saved = this.persistence.loadMessages();
if (saved.length > 0) {
for (const msg of saved) {
await this.appendMessage(msg.role, msg.content);
this._restoring = true;
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(),
};
this.chatState.messages.push(msg);
// Fire-and-forget save — don't block rendering
this.persistence?.saveMessages(this.chatState.messages);
// Fire-and-forget save — don't block rendering.
// Suppressed during restore (too many racing saveData writes).
if (!this._restoring) {
this.persistence?.saveMessages(this.chatState.messages);
}
const wrapper = this.messagesContainer.createDiv("chat-message");
wrapper.addClass(`chat-message-${role}`);