fix: eliminate saveData race condition with sessionId
Root cause: appendMessage() fired fire-and-forget saveMessages() on every message. In handleSend(), the user message save raced against the later sessionId save — whichever completed last won. Since the user message save fired first (before the API response), if it completed last it overwrote chatSessionId back to empty. Fixes: - Removed per-message saveMessages from appendMessage(). Persistence now happens in two places only: handleSend() after full exchange, and onClose() on shutdown. - saveSessionId no longer calls saveData — it stamps the value on settings and lets the subsequent saveMessages() persist both fields in a single atomic write. - saveSessionId is awaited before saveMessages so ordering is guaranteed: sessionId is on settings when saveData fires. - Added request logging: [Hermes API] shows URL, model, message count, and session ID for every API call. - Added restore logging: [Hermes] shows whether session was restored or started fresh. The Marco/Bolo test should now work — sessionId survives restart.
This commit is contained in:
parent
7b857803a7
commit
16f4bb589a
4 changed files with 30 additions and 13 deletions
10
main.js
10
main.js
File diff suppressed because one or more lines are too long
|
|
@ -134,6 +134,11 @@ export class ApiClient {
|
|||
headers["X-Hermes-Session-Id"] = this.currentSessionId;
|
||||
}
|
||||
|
||||
console.log("[Hermes API] POST", `${this.baseUrl}/chat/completions`,
|
||||
"| model:", this.model,
|
||||
"| msgs:", messages.length,
|
||||
"| session:", this.currentSessionId ? this.currentSessionId.slice(0, 8) + "..." : "(none)");
|
||||
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/chat/completions`,
|
||||
{
|
||||
|
|
@ -193,6 +198,11 @@ export class ApiClient {
|
|||
headers["X-Hermes-Session-Id"] = this.currentSessionId;
|
||||
}
|
||||
|
||||
console.log("[Hermes API] POST", `${this.baseUrl}/chat/completions`,
|
||||
"| model:", this.model,
|
||||
"| msgs:", messages.length,
|
||||
"| session:", this.currentSessionId ? this.currentSessionId.slice(0, 8) + "..." : "(none)");
|
||||
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/chat/completions`,
|
||||
{
|
||||
|
|
@ -215,6 +225,7 @@ export class ApiClient {
|
|||
const sessionId = response.headers.get("X-Hermes-Session-Id");
|
||||
if (sessionId) {
|
||||
this.currentSessionId = sessionId;
|
||||
console.log("[Hermes API] New session:", sessionId.slice(0, 8) + "...");
|
||||
}
|
||||
|
||||
// Read the SSE stream
|
||||
|
|
|
|||
|
|
@ -259,11 +259,8 @@ export class ChatView extends ItemView {
|
|||
timestamp: Date.now(),
|
||||
};
|
||||
this.chatState.messages.push(msg);
|
||||
// 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);
|
||||
}
|
||||
// Persistence is handled by handleSend() (after full exchange) and onClose().
|
||||
// Per-message saves created race conditions with sessionId persistence.
|
||||
|
||||
const wrapper = this.messagesContainer.createDiv("chat-message");
|
||||
wrapper.addClass(`chat-message-${role}`);
|
||||
|
|
@ -544,13 +541,17 @@ export class ChatView extends ItemView {
|
|||
timestamp: Date.now(),
|
||||
};
|
||||
this.chatState.messages.push(assistantMsg);
|
||||
this.persistence?.saveMessages(this.chatState.messages);
|
||||
|
||||
if (response.sessionId) {
|
||||
this.chatState.sessionId = response.sessionId;
|
||||
this.updateStatus(response.model, response.sessionId);
|
||||
this.persistence?.saveSessionId(response.sessionId);
|
||||
// Set on settings object before saveMessages so both are persisted
|
||||
// in a single saveData() call — no racing writes.
|
||||
await this.persistence?.saveSessionId(response.sessionId);
|
||||
}
|
||||
|
||||
// Single persistence call: messages + sessionId (if set above) in one write
|
||||
await this.persistence?.saveMessages(this.chatState.messages);
|
||||
} catch (error: any) {
|
||||
if (contentDiv) {
|
||||
contentDiv.setText(`Error: ${error.message || "Unknown error"}`);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ export default class HermesChatPlugin extends Plugin {
|
|||
loadSessionId: () => this.settings.chatSessionId ?? "",
|
||||
saveSessionId: async (id) => {
|
||||
this.settings.chatSessionId = id;
|
||||
await this.saveData(this.settings);
|
||||
// Don't call saveData here — saveMessages handles the write.
|
||||
// Both chatMessages and chatSessionId live on this.settings,
|
||||
// so a single saveData() call persists them atomically.
|
||||
},
|
||||
};
|
||||
const view = new ChatView(leaf, client, this.contextBridge!, persistence);
|
||||
|
|
@ -39,6 +41,9 @@ export default class HermesChatPlugin extends Plugin {
|
|||
if (this.settings.chatSessionId) {
|
||||
client.setSessionId(this.settings.chatSessionId);
|
||||
view.updateStatus("hermes-agent", this.settings.chatSessionId);
|
||||
console.log("[Hermes] Restored session:", this.settings.chatSessionId.slice(0, 8) + "...");
|
||||
} else {
|
||||
console.log("[Hermes] No saved session — starting fresh");
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue