evaluateProTurnCompletion never receives a branch-selection pointer. When a conversation mapping contains two independent, both-complete recap -> terminal-text branches under the same turn_exchange_id — the shape produced by a regenerate/retry — the leaf-ambiguity check rejects both because neither is a graph descendant of the other:
const leafCandidates = structurallySafe.filter((candidate) =>
!structurallySafe.some((other) =>
other.key !== candidate.key && isDescendantOf(mapping, other.key, candidate.key)
)
);
if (leafCandidates.length !== 1) {
return {
done: false,
reason: `ambiguous terminal text branches (${leafCandidates.length})`,
};
}
That is correct for a genuinely stray/abandoned branch, but it is a false negative when both branches are finished and the UI is currently displaying one specific branch. From mapping alone the function cannot distinguish those cases.
Reproduction
root
├─ recapA(reasoning_ended)
│ └─ finalA(finished_successfully, end_turn, stop)
└─ recapB(reasoning_ended)
└─ finalB(finished_successfully, end_turn, stop)
Both branches use the same turn_exchange_id. Current result (executed against a logic-identical port of pro-final.ts):
{
"done": false,
"reason": "active or graph-incomparable reasoning remains for final text candidate"
}
(The exact reason is the active-reasoning/graph-incomparability veto rather than the later ambiguous terminal text branches (2) check, but the underlying issue is the same: there is no branch-selection signal.)
Possible direction
ChatGPT conversation responses are commonly understood to include a top-level current_node pointer identifying the branch the UI displays, but I have not verified that field against a captured response from rosetta's current endpoint, so treating it as available would be premature. client.ts currently narrows the response to {mapping?: ConversationMapping}, and neither src/ nor tests/ references current_node.
Could you confirm whether the raw /backend-api/conversation/<id> response you see carries current_node? If it does, one option is to widen the response type and pass that pointer into the evaluator as an optional tie-break. If it does not, this may be an unavoidable fail-closed case and worth documenting as such.
I don't have a patch ready for this one because the right API depends on that wire fact. Filed separately from #1 because #1 is a one-line fail-open fix with a ready regression, while this is a caller/API design question.
evaluateProTurnCompletionnever receives a branch-selection pointer. When a conversation mapping contains two independent, both-completerecap -> terminal-textbranches under the sameturn_exchange_id— the shape produced by a regenerate/retry — the leaf-ambiguity check rejects both because neither is a graph descendant of the other:That is correct for a genuinely stray/abandoned branch, but it is a false negative when both branches are finished and the UI is currently displaying one specific branch. From
mappingalone the function cannot distinguish those cases.Reproduction
Both branches use the same
turn_exchange_id. Current result (executed against a logic-identical port ofpro-final.ts):{ "done": false, "reason": "active or graph-incomparable reasoning remains for final text candidate" }(The exact reason is the active-reasoning/graph-incomparability veto rather than the later
ambiguous terminal text branches (2)check, but the underlying issue is the same: there is no branch-selection signal.)Possible direction
ChatGPT conversation responses are commonly understood to include a top-level
current_nodepointer identifying the branch the UI displays, but I have not verified that field against a captured response from rosetta's current endpoint, so treating it as available would be premature.client.tscurrently narrows the response to{mapping?: ConversationMapping}, and neithersrc/nortests/referencescurrent_node.Could you confirm whether the raw
/backend-api/conversation/<id>response you see carriescurrent_node? If it does, one option is to widen the response type and pass that pointer into the evaluator as an optional tie-break. If it does not, this may be an unavoidable fail-closed case and worth documenting as such.I don't have a patch ready for this one because the right API depends on that wire fact. Filed separately from #1 because #1 is a one-line fail-open fix with a ready regression, while this is a caller/API design question.