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;
|
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(
|
const response = await this.fetchWithTimeout(
|
||||||
`${this.baseUrl}/chat/completions`,
|
`${this.baseUrl}/chat/completions`,
|
||||||
{
|
{
|
||||||
|
|
@ -193,6 +198,11 @@ export class ApiClient {
|
||||||
headers["X-Hermes-Session-Id"] = this.currentSessionId;
|
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(
|
const response = await this.fetchWithTimeout(
|
||||||
`${this.baseUrl}/chat/completions`,
|
`${this.baseUrl}/chat/completions`,
|
||||||
{
|
{
|
||||||
|
|
@ -215,6 +225,7 @@ export class ApiClient {
|
||||||
const sessionId = response.headers.get("X-Hermes-Session-Id");
|
const sessionId = response.headers.get("X-Hermes-Session-Id");
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
this.currentSessionId = sessionId;
|
this.currentSessionId = sessionId;
|
||||||
|
console.log("[Hermes API] New session:", sessionId.slice(0, 8) + "...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the SSE stream
|
// Read the SSE stream
|
||||||
|
|
|
||||||
|
|
@ -259,11 +259,8 @@ 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.
|
// Persistence is handled by handleSend() (after full exchange) and onClose().
|
||||||
// Suppressed during restore (too many racing saveData writes).
|
// Per-message saves created race conditions with sessionId persistence.
|
||||||
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}`);
|
||||||
|
|
@ -544,13 +541,17 @@ export class ChatView extends ItemView {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
this.chatState.messages.push(assistantMsg);
|
this.chatState.messages.push(assistantMsg);
|
||||||
this.persistence?.saveMessages(this.chatState.messages);
|
|
||||||
|
|
||||||
if (response.sessionId) {
|
if (response.sessionId) {
|
||||||
this.chatState.sessionId = response.sessionId;
|
this.chatState.sessionId = response.sessionId;
|
||||||
this.updateStatus(response.model, 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) {
|
} catch (error: any) {
|
||||||
if (contentDiv) {
|
if (contentDiv) {
|
||||||
contentDiv.setText(`Error: ${error.message || "Unknown error"}`);
|
contentDiv.setText(`Error: ${error.message || "Unknown error"}`);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ export default class HermesChatPlugin extends Plugin {
|
||||||
loadSessionId: () => this.settings.chatSessionId ?? "",
|
loadSessionId: () => this.settings.chatSessionId ?? "",
|
||||||
saveSessionId: async (id) => {
|
saveSessionId: async (id) => {
|
||||||
this.settings.chatSessionId = 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);
|
const view = new ChatView(leaf, client, this.contextBridge!, persistence);
|
||||||
|
|
@ -39,6 +41,9 @@ export default class HermesChatPlugin extends Plugin {
|
||||||
if (this.settings.chatSessionId) {
|
if (this.settings.chatSessionId) {
|
||||||
client.setSessionId(this.settings.chatSessionId);
|
client.setSessionId(this.settings.chatSessionId);
|
||||||
view.updateStatus("hermes-agent", 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;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue