Merge pull request #6950 from redpersongpt/codex/group-peer-history-identity

fix(group): preserve peer identity when reloading history
This commit is contained in:
Alexandre Teixeira
2026-10-30 19:03:48 +00:00
committed by GitHub
3 changed files with 84 additions and 4 deletions
+4 -2
View File
@@ -801,7 +801,8 @@ async function _sendRoundRobin(msg, box) {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [{ body: JSON.stringify({ messages: [{
role: 'user', role: 'user',
content: `[${m._groupName || m.display}]: ${response}` content: `[${m._groupName || m.display}]: ${response}`,
metadata: { group_peer: true, group_model: m._groupName || m.display, model: m.mid }
}]}), }]}),
}); });
} catch (e) { console.warn('[group] sync failed:', e); } } catch (e) { console.warn('[group] sync failed:', e); }
@@ -828,7 +829,8 @@ async function _syncAllResponses(holders) {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [{ body: JSON.stringify({ messages: [{
role: 'user', role: 'user',
content: `[${model._groupName || model.display}]: ${response}` content: `[${model._groupName || model.display}]: ${response}`,
metadata: { group_peer: true, group_model: model._groupName || model.display, model: model.mid }
}]}), }]}),
}); });
} catch (e) { /* silent */ } } catch (e) { /* silent */ }
+13 -2
View File
@@ -299,7 +299,18 @@ function _renderHistoryMessage(msg, modelName) {
} else { } else {
displayContent = ''; displayContent = '';
} }
if (msg.role === 'user') { let displayRole = msg.role;
// Peer turns remain user-role context for the model, but are agent replies
// in history. Only explicit identity plus its exact prefix can opt in.
if (displayRole === 'user' && meta?.group_peer === true
&& typeof meta.group_model === 'string' && meta.group_model) {
const prefix = `[${meta.group_model}]: `;
if (displayContent.startsWith(prefix)) {
displayRole = 'assistant';
displayContent = displayContent.slice(prefix.length);
}
}
if (displayRole === 'user') {
displayContent = _stripUserVisionBlocks(displayContent); displayContent = _stripUserVisionBlocks(displayContent);
const trimmed = displayContent.trim(); const trimmed = displayContent.trim();
if ( if (
@@ -316,7 +327,7 @@ function _renderHistoryMessage(msg, modelName) {
displayContent = `[Doc edit: ${docEditMatch[1]}] ${docEditMatch[3]}`; displayContent = `[Doc edit: ${docEditMatch[1]}] ${docEditMatch[3]}`;
} }
} }
return _addHistoryMessageWithFullRenderer(msg.role, displayContent, modelName, meta); return _addHistoryMessageWithFullRenderer(displayRole, displayContent, modelName, meta);
} }
function _clearHistoryPager() { function _clearHistoryPager() {
+67
View File
@@ -0,0 +1,67 @@
"""Peer injections retain their context role but hydrate as the named agent."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from tests.helpers.js_modules import js_function_source
ROOT = Path(__file__).resolve().parents[1]
def _node(script, *args):
result = subprocess.run(['node', '--input-type=module', '-e', script, *map(json.dumps, args)],
cwd=ROOT, capture_output=True, text=True, timeout=15)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
@pytest.mark.skipif(not shutil.which('node'), reason='node not installed')
@pytest.mark.parametrize('mode', ['parallel', 'round-robin'])
def test_peer_sync_persists_explicit_identity(mode):
source = (ROOT / 'static/js/group.js').read_text()
functions = '\n'.join(js_function_source(name, source) for name in ['_syncAllResponses', '_sendRoundRobin'])
data = _node(r'''
const source=JSON.parse(process.argv[1]),mode=JSON.parse(process.argv[2]);
const _models=[{mid:'model-a',display:'Model A',_groupName:'Persona A'}, {mid:'model-b',display:'Model B'}];
const _participantSessions=['a','b'],API_BASE='http://app',sent=[];
let _abortControllers=[];
const uiModule={scrollHistory(){}};
function _createGroupBubble(model){return {dataset:{raw:'Reply from '+model.mid}};}
async function _streamToHolder(){} function _saveState(){}
async function fetch(url,options){sent.push({url,...JSON.parse(options.body)});return {ok:true};}
eval(source+'\nglobalThis.runSync=async()=> mode==="parallel" ? _syncAllResponses(_models.map(_createGroupBubble)) : _sendRoundRobin("Hi",null);');
await globalThis.runSync();console.log(JSON.stringify(sent));
''', functions, mode)
assert len(data) == 2
for item in data:
message = item['messages'][0]
assert message['role'] == 'user' # Model context must retain peer attribution.
peer = 'model-b' if item['url'].endswith('/a/inject_messages') else 'model-a'
name = 'Model B' if peer == 'model-b' else 'Persona A'
assert message['content'] == f'[{name}]: Reply from {peer}'
assert message['metadata'] == {'group_peer': True, 'group_model': name, 'model': peer}
@pytest.mark.skipif(not shutil.which('node'), reason='node not installed')
@pytest.mark.parametrize('role,content,meta,expected_role,expected_content', [
('user', '[Persona A]: Reply', {'group_peer': True, 'group_model': 'Persona A'}, 'assistant', 'Reply'),
('user', '[Persona A]: Human text', {}, 'user', '[Persona A]: Human text'),
('user', 'Human text', {}, 'user', 'Human text'),
('assistant', 'Parent reply', {'group_model': 'Persona A'}, 'assistant', 'Parent reply'),
('user', 'No matching prefix', {'group_peer': True, 'group_model': 'Persona A'}, 'user', 'No matching prefix'),
('user', '[3]: Human text', {'group_peer': True, 'group_model': 3}, 'user', '[3]: Human text'),
('user', '[Persona A]: Human text', {'group_peer': False, 'group_model': 'Persona A'}, 'user', '[Persona A]: Human text'),
])
def test_history_uses_explicit_peer_identity_only(role, content, meta, expected_role, expected_content):
source = js_function_source('_renderHistoryMessage', (ROOT / 'static/js/sessions.js').read_text())
result = _node(r'''
const source=JSON.parse(process.argv[1]),message=JSON.parse(process.argv[2]);
function _displayHistoryContent(text){return text;}function _stripUserVisionBlocks(text){return text;}
function _addHistoryMessageWithFullRenderer(role,content,model,metadata){return {role,content,metadata};}
const run=eval('('+source+')');console.log(JSON.stringify(run(message,'session-model')));
''', source, {'role': role, 'content': content, 'metadata': meta})
assert result['role'] == expected_role
assert result['content'] == expected_content
assert result['metadata'] == {**meta, '_fromHistory': True}