mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-18 13:40:22 +00:00
fix(#4811): regenerate row_id/seq from final order index so anonymous tool rows don't collide (gate finding)
Codex gate found a SILENT drop: reconnect scene rows are built with orderIndex=0, so two tools WITHOUT a tool id at the same message index both got row_id '...:tool:<idx>:0'; _completeSettledAnchorSceneForTurn dedupes by row_id, silently dropping the second. Now the emit loop rewrites order_index AND regenerates the index-derived row_id + seq + identity.seq from the final per-bucket position (tid-based tool row_ids, already unique, are left untouched). Regression test added + proven non-vacuous. Co-authored-by: Rod Boev <rod.boev@gmail.com>
This commit is contained in:
+16
-2
@@ -2602,10 +2602,24 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(aTime!==bTime) return aTime-bTime;
|
||||
return a._encounter-b._encounter;
|
||||
});
|
||||
// Emit with sequential order_index values, strip temp props
|
||||
// Emit with sequential order_index values, strip temp props.
|
||||
// Rows were built with orderIndex=0, so their row_id/seq still encode 0.
|
||||
// Rewrite order_index AND regenerate the index-derived identity fields
|
||||
// (row_id/seq) from the final per-bucket position, so two anonymous rows
|
||||
// (no tool id) at the same message index don't collide on the same row_id
|
||||
// and get silently deduped by _completeSettledAnchorSceneForTurn().
|
||||
for(const row of pool){
|
||||
const {_phase,_encounter,...clean}=row;
|
||||
clean.order_index=byIdx.has(idx)?byIdx.get(idx).length:0;
|
||||
const oi=byIdx.has(idx)?byIdx.get(idx).length:0;
|
||||
clean.order_index=oi;
|
||||
clean.seq=oi;
|
||||
if(clean.identity&&typeof clean.identity==='object') clean.identity={...clean.identity,seq:oi};
|
||||
// Tool rows with a tool id carry a tid-based row_id (already unique) —
|
||||
// only regenerate the default index-based row_id form.
|
||||
const indexRowId=`settled:${activeSid||'session'}:${streamId||'stream'}:${clean.role}:${idx}:0`;
|
||||
if(clean.row_id===indexRowId){
|
||||
clean.row_id=`settled:${activeSid||'session'}:${streamId||'stream'}:${clean.role}:${idx}:${oi}`;
|
||||
}
|
||||
add(idx,clean);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,3 +299,35 @@ def test_order_index_sequential_within_bucket(driver_path):
|
||||
rows = out["1"]
|
||||
indices = [r["order_index"] for r in rows]
|
||||
assert indices == list(range(len(rows))), f"order_index not sequential: {indices}"
|
||||
|
||||
|
||||
def test_anonymous_tool_rows_get_distinct_row_ids(driver_path):
|
||||
"""Two tools WITHOUT an id at the same message index must not collide on the
|
||||
same row_id (which would let _completeSettledAnchorSceneForTurn dedupe one
|
||||
away). row_id/seq are regenerated from the final per-bucket order index.
|
||||
|
||||
Regression guard for the gate-found SILENT drop: rows are built with
|
||||
orderIndex=0, so the index-derived row_id/seq must be rewritten on emit."""
|
||||
messages = [
|
||||
{"role": "user", "content": "go"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
# two anonymous tools (no 'id') at the same index, distinct timestamps
|
||||
"tool_calls": [
|
||||
{"name": "terminal", "done": True, "started_at": 100},
|
||||
{"name": "read_file", "done": True, "started_at": 200},
|
||||
],
|
||||
},
|
||||
{"role": "assistant", "content": "final"},
|
||||
]
|
||||
payload = {"messages": messages, "turnStart": 0, "lastAsstIndex": 2, "S": {"toolCalls": []}}
|
||||
out = _run(driver_path, payload)
|
||||
rows = out["1"]
|
||||
tool_rows = [r for r in rows if r.get("role") == "tool"]
|
||||
assert len(tool_rows) == 2, f"both anonymous tool rows must survive, got {len(tool_rows)}"
|
||||
row_ids = [r["row_id"] for r in tool_rows]
|
||||
assert len(set(row_ids)) == 2, f"anonymous tool rows collided on row_id: {row_ids}"
|
||||
seqs = [r.get("seq") for r in tool_rows]
|
||||
assert len(set(seqs)) == 2, f"anonymous tool rows collided on seq: {seqs}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user