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:
nesquena-hermes
2026-06-24 19:48:56 +00:00
parent f330fa7f44
commit 3c8cfdc589
2 changed files with 48 additions and 2 deletions
+16 -2
View File
@@ -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}"