@@ -27,6 +27,7 @@ export class CoordinatorProvider extends BaseProvider {
2727 agentRegistry = new Map ( ) ,
2828 negotiator = null ,
2929 primaryRoomJid = null ,
30+ logRoomJid = process . env . LOG_ROOM_JID || "log@conference.tensegrity.it" ,
3031 enableDebate = false ,
3132 debateTimeoutMs = null ,
3233 contributionTimeoutMs = null ,
@@ -41,6 +42,7 @@ export class CoordinatorProvider extends BaseProvider {
4142 this . agentRegistry = agentRegistry ;
4243 this . negotiator = negotiator ;
4344 this . primaryRoomJid = primaryRoomJid ;
45+ this . logRoomJid = logRoomJid ;
4446 this . enableDebate = enableDebate ;
4547 this . debateTimeoutMs = debateTimeoutMs ;
4648 this . contributionTimeoutMs = contributionTimeoutMs ;
@@ -79,6 +81,9 @@ export class CoordinatorProvider extends BaseProvider {
7981 case "start" :
8082 return await this . startMfrSession ( content , metadata , reply ) ;
8183
84+ case "mfr-consensus" :
85+ return await this . startConsensusSession ( content , metadata , reply ) ;
86+
8287 case "mfr-debate" :
8388 case "debate" :
8489 if ( ! this . enableDebate ) {
@@ -111,6 +116,12 @@ export class CoordinatorProvider extends BaseProvider {
111116 return this . getHelpMessage ( ) ;
112117
113118 default :
119+ if ( command === "chat" ) {
120+ const consensusSessionId = this . getActiveConsensusSessionId ( ) ;
121+ if ( consensusSessionId ) {
122+ this . recordConsensusEntry ( consensusSessionId , rawMessage , metadata ) ;
123+ }
124+ }
114125 if ( this . enableDebate ) {
115126 const consensusResult = await this . handleDebateConsensus (
116127 content || rawMessage || "" ,
@@ -282,6 +293,83 @@ export class CoordinatorProvider extends BaseProvider {
282293 : `Debate started: ${ sessionId } . Window: ${ debateSeconds } s.` ;
283294 }
284295
296+ /**
297+ * Start consensus session for community answers
298+ * @param {string } problemDescription - Problem description
299+ * @param {Object } metadata - Message metadata
300+ * @param {Function } reply - Reply function
301+ * @returns {Promise<string> } Response message
302+ */
303+ async startConsensusSession ( problemDescription , metadata , reply ) {
304+ const sessionId = randomUUID ( ) ;
305+ const verbose = isVerboseRequest ( problemDescription ) ;
306+ const quiet = isQuietRequest ( problemDescription ) ;
307+
308+ this . logger . info ?. (
309+ `[CoordinatorProvider] Starting consensus session: ${ sessionId } `
310+ ) ;
311+
312+ const state = new MfrProtocolState ( sessionId , {
313+ logger : this . logger ,
314+ initialPhase : MFR_PHASES . PROBLEM_INTERPRETATION
315+ } ) ;
316+ this . activeSessions . set ( sessionId , state ) ;
317+
318+ state . transition ( MFR_PHASES . CONSENSUS_DISCOVERY , {
319+ problemDescription,
320+ startedBy : metadata ?. sender ,
321+ verbose,
322+ quiet
323+ } ) ;
324+
325+ this . activeDebates . set ( sessionId , {
326+ mode : "consensus" ,
327+ problemDescription,
328+ startTime : Date . now ( ) ,
329+ positions : [ ] ,
330+ rawMessages : [ ] ,
331+ consensusReached : false ,
332+ selectedAgents : [ ] ,
333+ verbose,
334+ quiet
335+ } ) ;
336+
337+ const agentMentions = Array . from ( this . agentRegistry . values ( ) || [ ] )
338+ . map ( ( entry ) => entry ?. nickname )
339+ . filter ( Boolean )
340+ . filter ( ( name ) => name . toLowerCase ( ) !== "coordinator" )
341+ . map ( ( name ) => `@${ name } ` )
342+ . join ( " " ) ;
343+
344+ const consensusIssue = [
345+ `Session: ${ sessionId } ` ,
346+ `Issue: Please provide Position/Support/Objection for the following question.` ,
347+ `Question: ${ problemDescription } ` ,
348+ agentMentions ? `Agents: ${ agentMentions } ` : null ,
349+ agentMentions ? `Agents listed above: reply in this room with "Position:", "Support:", or "Objection:" (one or more lines).` : null ,
350+ `Respond with Position/Support/Objection.` ,
351+ ``
352+ ] . filter ( Boolean ) . join ( "\n" ) ;
353+
354+ await this . sendStatusMessage ( consensusIssue , { forceChat : true } ) ;
355+
356+ if ( ! Number . isFinite ( this . debateTimeoutMs ) ) {
357+ throw new Error ( "Consensus timeout missing or invalid; check profile mfrConfig." ) ;
358+ }
359+
360+ setTimeout ( async ( ) => {
361+ await this . concludeConsensus ( sessionId ) ;
362+ } , this . debateTimeoutMs ) ;
363+
364+ const consensusSeconds = Math . round ( this . debateTimeoutMs / 1000 ) ;
365+ if ( quiet ) {
366+ return `Consensus started: ${ sessionId } .` ;
367+ }
368+ return verbose
369+ ? `Consensus session started: ${ sessionId } \n\nWindow: ${ consensusSeconds } seconds\nWaiting for Position/Support/Objection entries...`
370+ : `Consensus started: ${ sessionId } . Window: ${ consensusSeconds } s.` ;
371+ }
372+
285373 /**
286374 * Conclude debate and transition to entity discovery
287375 * @param {string } sessionId - Session ID
@@ -305,6 +393,11 @@ export class CoordinatorProvider extends BaseProvider {
305393 `[CoordinatorProvider] Concluding debate for ${ sessionId } `
306394 ) ;
307395
396+ if ( debateData . mode === "consensus" ) {
397+ await this . concludeConsensus ( sessionId ) ;
398+ return ;
399+ }
400+
308401 // For now, proceed with all agents (consensus detection to be added with Chair integration)
309402 const selected = Array . isArray ( selectedAgents ) && selectedAgents . length > 0
310403 ? selectedAgents
@@ -1035,6 +1128,18 @@ export class CoordinatorProvider extends BaseProvider {
10351128 }
10361129 }
10371130
1131+ async sendLogMessage ( message ) {
1132+ if ( ! message ) return ;
1133+ if ( ! this . logRoomJid || ! this . negotiator ?. xmppClient ) return ;
1134+ await this . negotiator . xmppClient . send (
1135+ xml (
1136+ "message" ,
1137+ { to : this . logRoomJid , type : "groupchat" } ,
1138+ xml ( "body" , { } , message )
1139+ )
1140+ ) ;
1141+ }
1142+
10381143 getSessionVerbose ( sessionId ) {
10391144 const debateData = this . activeDebates . get ( sessionId ) ;
10401145 if ( debateData ?. verbose ) return true ;
@@ -1126,6 +1231,22 @@ export class CoordinatorProvider extends BaseProvider {
11261231 getActiveDebateSessionId ( ) {
11271232 let latest = null ;
11281233 for ( const [ sessionId , debateData ] of this . activeDebates . entries ( ) ) {
1234+ if ( debateData . mode === "consensus" ) {
1235+ continue ;
1236+ }
1237+ if ( ! latest || debateData . startTime > latest . startTime ) {
1238+ latest = { sessionId, startTime : debateData . startTime } ;
1239+ }
1240+ }
1241+ return latest ?. sessionId || null ;
1242+ }
1243+
1244+ getActiveConsensusSessionId ( ) {
1245+ let latest = null ;
1246+ for ( const [ sessionId , debateData ] of this . activeDebates . entries ( ) ) {
1247+ if ( debateData . mode !== "consensus" ) {
1248+ continue ;
1249+ }
11291250 if ( ! latest || debateData . startTime > latest . startTime ) {
11301251 latest = { sessionId, startTime : debateData . startTime } ;
11311252 }
@@ -1184,6 +1305,114 @@ export class CoordinatorProvider extends BaseProvider {
11841305 : `Consensus received for ${ sessionId } .` ;
11851306 }
11861307
1308+ recordConsensusEntry ( sessionId , message , metadata = { } ) {
1309+ const debateData = this . activeDebates . get ( sessionId ) ;
1310+ if ( ! debateData || debateData . mode !== "consensus" ) return ;
1311+
1312+ const entries = parseConsensusEntries ( message || "" ) ;
1313+ if ( entries . length === 0 ) {
1314+ const rawMessages = debateData . rawMessages || [ ] ;
1315+ if ( rawMessages . length < 200 ) {
1316+ rawMessages . push ( {
1317+ sender : metadata ?. sender || "unknown" ,
1318+ text : String ( message || "" ) . trim ( ) ,
1319+ timestamp : new Date ( ) . toISOString ( )
1320+ } ) ;
1321+ debateData . rawMessages = rawMessages ;
1322+ }
1323+ return ;
1324+ }
1325+
1326+ entries . forEach ( ( entry ) => {
1327+ debateData . positions . push ( {
1328+ ...entry ,
1329+ sender : metadata ?. sender || "unknown" ,
1330+ timestamp : new Date ( ) . toISOString ( )
1331+ } ) ;
1332+ } ) ;
1333+ }
1334+
1335+ async concludeConsensus ( sessionId ) {
1336+ const state = this . activeSessions . get ( sessionId ) ;
1337+ const debateData = this . activeDebates . get ( sessionId ) ;
1338+
1339+ if ( ! state || ! debateData ) {
1340+ this . logger . warn ?. ( `[CoordinatorProvider] No active consensus for ${ sessionId } ` ) ;
1341+ return ;
1342+ }
1343+
1344+ if ( ! state . isPhase ( MFR_PHASES . CONSENSUS_DISCOVERY ) ) {
1345+ return ;
1346+ }
1347+
1348+ this . logger . info ?. (
1349+ `[CoordinatorProvider] Concluding consensus for ${ sessionId } `
1350+ ) ;
1351+
1352+ const summary = synthesizeConsensus ( debateData ) ;
1353+ const lines = [
1354+ `Consensus session complete: ${ sessionId } ` ,
1355+ `Answer: ${ summary . answer || "No clear consensus reached." } `
1356+ ] ;
1357+ if ( summary . support . length > 0 ) {
1358+ lines . push ( `Support: ${ summary . support . join ( " | " ) } ` ) ;
1359+ }
1360+ if ( summary . objections . length > 0 ) {
1361+ lines . push ( `Objections: ${ summary . objections . join ( " | " ) } ` ) ;
1362+ }
1363+
1364+ await this . sendStatusMessage ( lines . join ( "\n" ) , { sessionId, forceChat : true } ) ;
1365+ await this . sendConsensusLog ( sessionId , debateData , summary ) ;
1366+
1367+ state . transition ( MFR_PHASES . COMPLETE , {
1368+ consensus : summary . answer || null
1369+ } ) ;
1370+ this . activeDebates . delete ( sessionId ) ;
1371+ }
1372+
1373+ async sendConsensusLog ( sessionId , debateData , summary ) {
1374+ if ( ! debateData || debateData . mode !== "consensus" ) return ;
1375+ const question = debateData . problemDescription || "" ;
1376+ const entries = debateData . positions || [ ] ;
1377+ const rawMessages = debateData . rawMessages || [ ] ;
1378+ const lines = [
1379+ `[Consensus Log] ${ sessionId } ` ,
1380+ `Question: ${ question } ` ,
1381+ `Answer: ${ summary . answer || "No clear consensus reached." } ` ,
1382+ `Entries: ${ entries . length } ` ,
1383+ ""
1384+ ] ;
1385+
1386+ if ( entries . length > 0 ) {
1387+ lines . push ( "Parsed entries:" ) ;
1388+ entries . forEach ( ( entry , index ) => {
1389+ const sender = entry . sender || "unknown" ;
1390+ const type = entry . type || "note" ;
1391+ const text = entry . text || "" ;
1392+ const timestamp = entry . timestamp || "" ;
1393+ lines . push ( `${ index + 1 } . [${ type } ] ${ sender } ${ timestamp } ` . trim ( ) ) ;
1394+ if ( text ) {
1395+ lines . push ( ` ${ text } ` ) ;
1396+ }
1397+ } ) ;
1398+ }
1399+
1400+ if ( rawMessages . length > 0 ) {
1401+ lines . push ( "" , "Raw messages:" ) ;
1402+ rawMessages . slice ( 0 , 50 ) . forEach ( ( entry , index ) => {
1403+ const sender = entry . sender || "unknown" ;
1404+ const text = entry . text || "" ;
1405+ const timestamp = entry . timestamp || "" ;
1406+ lines . push ( `${ index + 1 } . ${ sender } ${ timestamp } ` . trim ( ) ) ;
1407+ if ( text ) {
1408+ lines . push ( ` ${ text } ` ) ;
1409+ }
1410+ } ) ;
1411+ }
1412+
1413+ await this . sendLogMessage ( lines . join ( "\n" ) ) ;
1414+ }
1415+
11871416 /**
11881417 * List all active sessions
11891418 * @param {Object } metadata - Metadata
@@ -1364,3 +1593,52 @@ function cleanTerm(value) {
13641593 . replace ( / [ ? . , ! ] + $ / g, "" )
13651594 . replace ( / \s + / g, " " ) ;
13661595}
1596+
1597+ function parseConsensusEntries ( message ) {
1598+ const text = String ( message || "" ) . trim ( ) ;
1599+ if ( ! text ) return [ ] ;
1600+ const entries = [ ] ;
1601+ const lines = text . split ( / \r ? \n / ) ;
1602+ const pattern = / ^ (?: [ - * • > ] \s * ) ? (?: \* { 1 , 2 } ) ? ( p o s i t i o n | s u p p o r t | o b j e c t i o n ) (?: \* { 1 , 2 } ) ? \s * [: \- – — ] \s * ( .+ ) $ / i;
1603+ for ( const line of lines ) {
1604+ const trimmed = line . trim ( ) ;
1605+ if ( ! trimmed ) continue ;
1606+ const match = trimmed . match ( pattern ) ;
1607+ if ( ! match ) continue ;
1608+ entries . push ( {
1609+ type : match [ 1 ] . toLowerCase ( ) ,
1610+ text : match [ 2 ] . trim ( )
1611+ } ) ;
1612+ }
1613+ return entries ;
1614+ }
1615+
1616+ function synthesizeConsensus ( debateData ) {
1617+ const positions = debateData . positions || [ ] ;
1618+ const positionCounts = new Map ( ) ;
1619+ const support = [ ] ;
1620+ const objections = [ ] ;
1621+
1622+ positions . forEach ( ( entry ) => {
1623+ if ( ! entry ?. text ) return ;
1624+ if ( entry . type === "position" ) {
1625+ const key = entry . text . toLowerCase ( ) ;
1626+ positionCounts . set ( key , {
1627+ count : ( positionCounts . get ( key ) ?. count || 0 ) + 1 ,
1628+ text : entry . text
1629+ } ) ;
1630+ } else if ( entry . type === "support" ) {
1631+ if ( support . length < 3 ) support . push ( entry . text ) ;
1632+ } else if ( entry . type === "objection" ) {
1633+ if ( objections . length < 3 ) objections . push ( entry . text ) ;
1634+ }
1635+ } ) ;
1636+
1637+ let answer = null ;
1638+ if ( positionCounts . size > 0 ) {
1639+ const sorted = Array . from ( positionCounts . values ( ) ) . sort ( ( a , b ) => b . count - a . count ) ;
1640+ answer = sorted [ 0 ] ?. text || null ;
1641+ }
1642+
1643+ return { answer, support, objections } ;
1644+ }
0 commit comments