fix: correct tool call card rendering on session load after context compaction (#408)

* fix: correct tool call card rendering on session load

Two bugs caused duplicate/incorrect tool call cards when loading
sessions (especially after context compaction):

1. loadSession() sanitized messages (B9 filter) but did NOT update
   the session-level tool_calls array's assistant_msg_idx references.
   Since compact() returns only sanitized messages and recomputes
   tool_calls with indices into the compacted array, the original
   assistant_msg_idx values became stale/misaligned.

2. loadSession() then assigned the broken session-level tool_calls
   directly to S.toolCalls. This prevented renderMessages()'s fallback
   path (which derives tool_calls from per-message tool_calls using
   correct sanitized-array indices) from ever running.

Fix:
- Keep full sanitization loop with index remapping for session-level
  tool_calls (in case they're needed by other code paths).
- Instead of assigning broken session-level tool_calls to S.toolCalls,
  set S.toolCalls=[] so renderMessages() uses the fallback derivation
  from per-message tool_calls, which already have correct indices.

* test: add 8 regression tests for issue #401 tool call index remapping

* docs: v0.50.29 release — version badge and CHANGELOG

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-13 22:41:31 -07:00
committed by GitHub
parent a2258139f2
commit d3fea34c41
4 changed files with 259 additions and 5 deletions

View File

@@ -42,6 +42,35 @@ async function loadSession(sid){
S.session=data.session;
S.lastUsage={...(data.session.last_usage||{})};
localStorage.setItem('hermes-webui-session',S.session.session_id);
// B9: sanitize empty assistant messages (PR #402) — build index map to remap
// session-level tool_calls.assistant_msg_idx to the new sanitized positions.
const allMsgs = data.session.messages || [];
const sanitized = [];
const origIdxToSanitizedIdx = {};
let lastKeptAsstIdx = -1;
for (let i = 0; i < allMsgs.length; i++) {
const m = allMsgs[i];
if (!m || !m.role) continue;
if (m.role === 'tool') continue;
if (m.role === 'assistant') {
let c = m.content || '';
if (Array.isArray(c)) c = c.filter(p => p && p.type === 'text').map(p => p.text || '').join('');
if (!String(c).trim().length) { continue; } // empty assistant — skip
lastKeptAsstIdx = sanitized.length;
}
origIdxToSanitizedIdx[i] = sanitized.length;
sanitized.push(m);
}
if (data.session.tool_calls && data.session.tool_calls.length) {
for (const tc of data.session.tool_calls) {
if (!tc || tc.assistant_msg_idx === undefined) continue;
const origIdx = tc.assistant_msg_idx;
tc.assistant_msg_idx = (origIdx in origIdxToSanitizedIdx)
? origIdxToSanitizedIdx[origIdx]
: (lastKeptAsstIdx >= 0 ? lastKeptAsstIdx : -1);
}
}
data.session.messages = sanitized;
const activeStreamId=data.session.active_stream_id||null;
if(!INFLIGHT[sid]&&activeStreamId&&typeof loadInflightState==='function'){
const stored=loadInflightState(sid, activeStreamId);
@@ -54,9 +83,6 @@ async function loadSession(sid){
};
}
}
// Keep raw session.messages intact so side panels (e.g. Todos) can still
// reconstruct state from tool outputs after reload. Visible transcript rows
// are filtered later by renderMessages().
if(INFLIGHT[sid]){
S.messages=INFLIGHT[sid].messages;
S.toolCalls=(INFLIGHT[sid].toolCalls||[]);
@@ -80,7 +106,11 @@ async function loadSession(sid){
S.messages=data.session.messages||[];
const pendingMsg=typeof getPendingSessionMessage==='function'?getPendingSessionMessage(data.session):null;
if(pendingMsg) S.messages.push(pendingMsg);
S.toolCalls=(data.session.tool_calls||[]).map(tc=>({...tc,done:true}));
// Fix (PR #402): do NOT pre-fill S.toolCalls from session-level tool_calls
// those have stale assistant_msg_idx values after B9 sanitization. Instead,
// set S.toolCalls=[] and let renderMessages() derive them from per-message
// tool_calls (which already have correct sanitized-array indices).
S.toolCalls=[];
clearLiveToolCards();
if(activeStreamId){
S.busy=true;