clay-server 2.26.0-beta.8 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,13 @@ var matesModule = require("./mates");
14
14
  */
15
15
  function attachDebate(ctx) {
16
16
 
17
+ // For mate projects, enforce latest debate awareness prompt in CLAUDE.md
18
+ // so mates use the propose_debate MCP tool instead of writing files.
19
+ if (ctx.isMate) {
20
+ var _debateClaudeMdPath = path.join(ctx.cwd, "CLAUDE.md");
21
+ try { matesModule.enforceDebateAwareness(_debateClaudeMdPath); } catch (e) {}
22
+ }
23
+
17
24
  // --- Helpers shared with other modules ---
18
25
 
19
26
  function escapeRegex(str) {
@@ -156,14 +163,16 @@ function attachDebate(ctx) {
156
163
  setupStartedAt: d.setupStartedAt || null,
157
164
  round: d.round || 1,
158
165
  awaitingConcludeConfirm: !!d.awaitingConcludeConfirm,
166
+ awaitingUserFloor: !!d.awaitingUserFloor,
167
+ ownerId: d.ownerId || null,
159
168
  };
160
169
  ctx.sm.saveSessionFile(session);
161
170
  }
162
171
 
163
- function restoreDebateFromState(session) {
172
+ function restoreDebateFromState(session, restoreUserId) {
164
173
  var ds = session.debateState;
165
174
  if (!ds) return null;
166
- var userId = null; // Will be set when WS connects
175
+ var userId = restoreUserId || ds.ownerId || null;
167
176
  var mateCtx = matesModule.buildMateCtx(userId);
168
177
  var debate = {
169
178
  phase: ds.phase,
@@ -186,6 +195,8 @@ function attachDebate(ctx) {
186
195
  setupStartedAt: ds.setupStartedAt || null,
187
196
  briefPath: ds.briefPath || null,
188
197
  awaitingConcludeConfirm: !!ds.awaitingConcludeConfirm,
198
+ awaitingUserFloor: !!ds.awaitingUserFloor,
199
+ ownerId: ds.ownerId || userId,
189
200
  };
190
201
 
191
202
  // Fallback: if awaitingConcludeConfirm was not persisted, detect from history
@@ -255,26 +266,20 @@ function attachDebate(ctx) {
255
266
  debate.context = brief.context || "";
256
267
  debate.specialRequests = brief.specialRequests || null;
257
268
 
258
- // Update panelists with roles from the brief
269
+ // Replace panelists with those selected in the brief
259
270
  if (brief.panelists && brief.panelists.length) {
260
- for (var i = 0; i < brief.panelists.length; i++) {
261
- var bp = brief.panelists[i];
262
- for (var j = 0; j < debate.panelists.length; j++) {
263
- if (debate.panelists[j].mateId === bp.mateId) {
264
- debate.panelists[j].role = bp.role || "";
265
- debate.panelists[j].brief = bp.brief || "";
266
- }
267
- }
268
- }
271
+ debate.panelists = brief.panelists.map(function (bp) {
272
+ return { mateId: bp.mateId, role: bp.role || "", brief: bp.brief || "" };
273
+ });
269
274
  }
270
275
 
271
276
  // Rebuild name map with updated roles
272
277
  var mateCtx = debate.mateCtx || matesModule.buildMateCtx(null);
273
278
  debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
274
279
 
275
- // If debate was started from DM (no setupSessionId), go to reviewing phase
276
- if (!debate.setupSessionId) {
277
- console.log("[debate] Brief picked up from DM, entering review phase. Topic:", debate.topic);
280
+ // quickStart from DM or no setupSessionId: show brief card for user approval
281
+ if (!debate.setupSessionId || debate.quickStart) {
282
+ console.log("[debate] Brief picked up, entering review phase. Topic:", debate.topic);
278
283
  debate.phase = "reviewing";
279
284
  persistDebateState(session);
280
285
 
@@ -325,88 +330,18 @@ function attachDebate(ctx) {
325
330
  // --- Restore debate on reconnect ---
326
331
 
327
332
  function restoreDebateState(ws) {
328
- var userId = ws._clayUser ? ws._clayUser.id : null;
329
- var mateCtx = matesModule.buildMateCtx(userId);
330
-
333
+ // On server restart, SDK sessions are lost so debates cannot continue.
334
+ // Clear stale debate state instead of restoring dead UI.
331
335
  ctx.sm.sessions.forEach(function (session) {
332
- // Already restored
333
- if (session._debate) return;
334
-
335
- // Has persisted debate state?
336
- if (!session.debateState) return;
337
-
338
- var phase = session.debateState.phase;
339
- if (phase !== "preparing" && phase !== "reviewing" && phase !== "live") return;
340
-
341
- // Restore _debate from persisted state
342
- var debate = restoreDebateFromState(session);
343
- if (!debate) return;
344
-
345
- // Update mateCtx with the connected user's context
346
- debate.mateCtx = mateCtx;
347
- debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
348
-
349
- var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
350
-
351
- if (phase === "preparing") {
352
- var briefPath = debate.briefPath;
353
- if (!briefPath && debate.debateId) {
354
- briefPath = path.join(ctx.cwd, ".clay", "debates", debate.debateId, "brief.json");
355
- }
356
- if (!briefPath) return;
357
-
358
- console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
359
- startDebateBriefWatcher(session, debate, briefPath);
360
-
361
- // Only show preparing indicator for quick start (standard setup shows skill in real-time)
362
- if (debate.quickStart) {
363
- ctx.sendTo(ws, {
364
- type: "debate_preparing",
365
- topic: debate.topic,
366
- moderatorId: debate.moderatorId,
367
- moderatorName: moderatorProfile.name,
368
- setupSessionId: debate.setupSessionId,
369
- panelists: debate.panelists.map(function (p) {
370
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
371
- return { mateId: p.mateId, name: prof.name };
372
- }),
373
- });
374
- }
375
- } else if (phase === "reviewing") {
376
- console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
377
- ctx.sendTo(ws, {
378
- type: "debate_brief_ready",
379
- debateId: debate.debateId,
380
- topic: debate.topic,
381
- format: debate.format || "free_discussion",
382
- context: debate.context || "",
383
- specialRequests: debate.specialRequests || null,
384
- moderatorId: debate.moderatorId,
385
- moderatorName: moderatorProfile.name,
386
- panelists: debate.panelists.map(function (p) {
387
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
388
- return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
389
- }),
390
- });
391
- } else if (phase === "live") {
392
- console.log("[debate] Restoring debate (live). topic:", debate.topic, "awaitingConclude:", debate.awaitingConcludeConfirm);
393
- // Debate was live when server restarted. It can't resume AI turns,
394
- // but we can show the sticky and let user see history.
395
- ctx.sendTo(ws, {
396
- type: "debate_started",
397
- topic: debate.topic,
398
- format: debate.format,
399
- round: debate.round,
400
- moderatorId: debate.moderatorId,
401
- moderatorName: moderatorProfile.name,
402
- panelists: debate.panelists.map(function (p) {
403
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
404
- return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
405
- }),
406
- });
407
- // If moderator had concluded, re-send conclude confirm so client shows End/Continue UI
408
- if (debate.awaitingConcludeConfirm) {
409
- ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
336
+ if (session._debate) {
337
+ delete session._debate;
338
+ }
339
+ if (session.debateState) {
340
+ var phase = session.debateState.phase;
341
+ if (phase === "preparing" || phase === "reviewing" || phase === "live") {
342
+ console.log("[debate] Clearing stale debate state:", session.debateState.topic);
343
+ session.debateState = null;
344
+ ctx.sm.saveSessionFile(session);
410
345
  }
411
346
  }
412
347
  });
@@ -470,6 +405,7 @@ function attachDebate(ctx) {
470
405
  setupSessionId: null,
471
406
  debateId: debateId,
472
407
  briefPath: briefPath,
408
+ ownerId: mateCtx.userId || null,
473
409
  };
474
410
  debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
475
411
  session._debate = debate;
@@ -500,8 +436,27 @@ function attachDebate(ctx) {
500
436
  var session = ctx.getSessionForWs(ws);
501
437
  if (!session) return;
502
438
 
503
- if (!msg.moderatorId || !msg.topic || !msg.panelists || !msg.panelists.length) {
504
- ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic, panelists." });
439
+ if (!msg.moderatorId || !msg.topic) {
440
+ ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic." });
441
+ return;
442
+ }
443
+
444
+ // delegatePanelists: moderator picks panelists, populate all available mates
445
+ if (msg.delegatePanelists) {
446
+ var userId = ws._clayUser ? ws._clayUser.id : null;
447
+ var tmpCtx = matesModule.buildMateCtx(userId);
448
+ var matesData = matesModule.loadMates(tmpCtx);
449
+ var allMates = matesData.mates || [];
450
+ msg.panelists = [];
451
+ for (var mi = 0; mi < allMates.length; mi++) {
452
+ if (allMates[mi].id !== msg.moderatorId && allMates[mi].status !== "interviewing") {
453
+ msg.panelists.push({ mateId: allMates[mi].id, role: "", brief: "" });
454
+ }
455
+ }
456
+ }
457
+
458
+ if (!msg.panelists || !msg.panelists.length) {
459
+ ctx.sendTo(ws, { type: "debate_error", error: "No panelists available." });
505
460
  return;
506
461
  }
507
462
 
@@ -538,6 +493,7 @@ function attachDebate(ctx) {
538
493
  round: 1,
539
494
  history: [],
540
495
  setupSessionId: null,
496
+ ownerId: userId,
541
497
  };
542
498
  session._debate = debate;
543
499
 
@@ -565,7 +521,8 @@ function attachDebate(ctx) {
565
521
  var debateId = debate.debateId;
566
522
 
567
523
  // Create setup session (still needed for session grouping)
568
- var setupSession = ctx.sm.createSession();
524
+ var setupOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
525
+ var setupSession = ctx.sm.createSession(setupOpts);
569
526
  setupSession.title = "Debate Setup: " + (msg.topic || "Quick").slice(0, 40);
570
527
  setupSession.debateSetupMode = true;
571
528
  setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: (msg.topic || "Quick").slice(0, 40), source: "debate", startedAt: Date.now() };
@@ -597,7 +554,9 @@ function attachDebate(ctx) {
597
554
  "",
598
555
  "## Your Task",
599
556
  "Based on the conversation context, create a debate brief. You know the topic well because you were just discussing it.",
600
- "Assign each panelist a role and perspective that will create the most productive debate.",
557
+ msg.delegatePanelists
558
+ ? "IMPORTANT: Select only 2-4 panelists who are most relevant to this specific topic. Do NOT include all of them. Be selective. Only pick mates whose expertise or personality directly contributes to this debate."
559
+ : "The user already selected these panelists. Assign each one a role and perspective that will create the most productive debate.",
601
560
  "",
602
561
  "Output ONLY a valid JSON object (no markdown fences, no extra text):",
603
562
  "{",
@@ -688,7 +647,8 @@ function attachDebate(ctx) {
688
647
  var debateId = debate.debateId;
689
648
 
690
649
  // Create a new session for the setup skill (like Ralph crafting)
691
- var setupSession = ctx.sm.createSession();
650
+ var skillSetupOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
651
+ var setupSession = ctx.sm.createSession(skillSetupOpts);
692
652
  setupSession.title = "Debate Setup: " + msg.topic.slice(0, 40);
693
653
  setupSession.debateSetupMode = true;
694
654
  setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: msg.topic.slice(0, 40), source: "debate", startedAt: Date.now() };
@@ -723,13 +683,24 @@ function attachDebate(ctx) {
723
683
  // Watch for brief.json in the debate-specific directory
724
684
  startDebateBriefWatcher(session, debate, briefPath);
725
685
 
726
- // Standard setup: no preparing indicator needed because the user
727
- // sees the skill working in real-time in the setup session.
686
+ // Notify clients that setup is in progress
687
+ var preparingMsg = {
688
+ type: "debate_preparing",
689
+ topic: debate.topic || "(Setting up...)",
690
+ moderatorId: debate.moderatorId,
691
+ moderatorName: moderatorProfile.name,
692
+ setupSessionId: setupSession.localId,
693
+ panelists: debate.panelists.map(function (p) {
694
+ var prof = ctx.getMateProfile(mateCtx, p.mateId);
695
+ return { mateId: p.mateId, name: prof.name };
696
+ }),
697
+ };
698
+ ctx.sendTo(ws, preparingMsg);
699
+ ctx.sendToSession(session.localId, preparingMsg);
728
700
 
729
- // Start the setup skill session
730
- setupSession.history.push({ type: "user_message", text: craftingPrompt });
731
- ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt });
732
- ctx.sendToSession(setupSession.localId, { type: "user_message", text: craftingPrompt });
701
+ // Start the setup skill session (don't send user_message to client — it's an internal prompt)
702
+ setupSession.history.push({ type: "user_message", text: craftingPrompt, _internal: true });
703
+ ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt, _internal: true });
733
704
  setupSession.isProcessing = true;
734
705
  ctx.onProcessingChanged();
735
706
  setupSession.sentToolResults = {};
@@ -737,6 +708,13 @@ function attachDebate(ctx) {
737
708
  ctx.sdk.startQuery(setupSession, craftingPrompt, undefined, ctx.getLinuxUserForSession(setupSession));
738
709
  }
739
710
 
711
+ // --- Mate strip processing indicator ---
712
+ // Broadcast mention_processing so the correct mate's active dot lights up
713
+ // on the mate strip during debate turns (instead of always the moderator's).
714
+ function debateMateProcessing(mateId, active) {
715
+ ctx.send({ type: "mention_processing", mateId: mateId, active: active });
716
+ }
717
+
740
718
  // --- Live debate ---
741
719
 
742
720
  function startDebateLive(session) {
@@ -751,13 +729,10 @@ function attachDebate(ctx) {
751
729
  var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
752
730
 
753
731
  // Create a dedicated debate session, grouped with the setup session
754
- var debateSession = ctx.sm.createSession();
732
+ var liveOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
733
+ var debateSession = ctx.sm.createSession(liveOpts);
755
734
  debateSession.title = debate.topic.slice(0, 50);
756
735
  debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
757
- // Assign cliSessionId manually so saveSessionFile works (no SDK query for debate sessions)
758
- if (!debateSession.cliSessionId) {
759
- debateSession.cliSessionId = crypto.randomUUID();
760
- }
761
736
  ctx.sm.saveSessionFile(debateSession);
762
737
  ctx.sm.switchSession(debateSession.localId, null, ctx.hydrateImageRefs);
763
738
  debate.liveSessionId = debateSession.localId;
@@ -789,6 +764,7 @@ function attachDebate(ctx) {
789
764
  ctx.sendToSession(debateSession.localId, debateStartEntry);
790
765
 
791
766
  // Signal moderator's first turn
767
+ debateMateProcessing(debate.moderatorId, true);
792
768
  ctx.sendToSession(debateSession.localId, {
793
769
  type: "debate_turn",
794
770
  mateId: debate.moderatorId,
@@ -843,6 +819,7 @@ function attachDebate(ctx) {
843
819
  var debate = session._debate;
844
820
  if (!debate || debate.phase === "ended") return;
845
821
 
822
+ debateMateProcessing(debate.moderatorId, false);
846
823
  debate.turnInProgress = false;
847
824
 
848
825
  // Record in debate history
@@ -886,6 +863,12 @@ function attachDebate(ctx) {
886
863
  return;
887
864
  }
888
865
 
866
+ // Check if user raised hand
867
+ if (debate.handRaised) {
868
+ yieldFloorToUser(session);
869
+ return;
870
+ }
871
+
889
872
  // Trigger the first mentioned panelist
890
873
  triggerPanelist(session, mentionedIds[0], fullText);
891
874
  }
@@ -908,6 +891,7 @@ function attachDebate(ctx) {
908
891
  }
909
892
  if (!panelistInfo) {
910
893
  console.error("[debate] Panelist not found:", mateId);
894
+ debateMateProcessing(mateId, false);
911
895
  debate._currentTurnMateId = null;
912
896
  // Feed error back to moderator
913
897
  feedBackToModerator(session, mateId, "[This panelist is not part of the debate panel.]");
@@ -915,6 +899,7 @@ function attachDebate(ctx) {
915
899
  }
916
900
 
917
901
  // Notify clients of new turn
902
+ debateMateProcessing(mateId, true);
918
903
  ctx.sendToSession(session.localId, {
919
904
  type: "debate_turn",
920
905
  mateId: mateId,
@@ -943,6 +928,7 @@ function attachDebate(ctx) {
943
928
  },
944
929
  onError: function (errMsg) {
945
930
  console.error("[debate] Panelist error for " + mateId + ":", errMsg);
931
+ debateMateProcessing(mateId, false);
946
932
  debate.turnInProgress = false;
947
933
  // Feed error back to moderator so the debate can continue
948
934
  feedBackToModerator(session, mateId, "[" + profile.name + " encountered an error and could not respond. Please continue with other panelists or wrap up.]");
@@ -1003,6 +989,7 @@ function attachDebate(ctx) {
1003
989
  }
1004
990
  }).catch(function (err) {
1005
991
  console.error("[debate] Failed to create panelist session for " + mateId + ":", err.message || err);
992
+ debateMateProcessing(mateId, false);
1006
993
  debate.turnInProgress = false;
1007
994
  feedBackToModerator(session, mateId, "[" + profile.name + " is unavailable. Please continue with other panelists or wrap up.]");
1008
995
  });
@@ -1013,6 +1000,7 @@ function attachDebate(ctx) {
1013
1000
  var debate = session._debate;
1014
1001
  if (!debate || debate.phase === "ended") return;
1015
1002
 
1003
+ debateMateProcessing(mateId, false);
1016
1004
  debate.turnInProgress = false;
1017
1005
  debate._currentTurnMateId = null;
1018
1006
  debate._currentTurnText = "";
@@ -1041,12 +1029,18 @@ function attachDebate(ctx) {
1041
1029
  return;
1042
1030
  }
1043
1031
 
1044
- // Check for pending user comment
1032
+ // Check for pending user comment (legacy)
1045
1033
  if (debate.pendingComment) {
1046
1034
  injectUserComment(session);
1047
1035
  return;
1048
1036
  }
1049
1037
 
1038
+ // Check if user raised hand (no comment, just wants the floor)
1039
+ if (debate.handRaised) {
1040
+ yieldFloorToUser(session);
1041
+ return;
1042
+ }
1043
+
1050
1044
  // Feed panelist response back to moderator
1051
1045
  feedBackToModerator(session, mateId, fullText);
1052
1046
  }
@@ -1070,6 +1064,7 @@ function attachDebate(ctx) {
1070
1064
  var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1071
1065
 
1072
1066
  // Notify clients of moderator turn
1067
+ debateMateProcessing(debate.moderatorId, true);
1073
1068
  ctx.sendToSession(session.localId, {
1074
1069
  type: "debate_turn",
1075
1070
  mateId: debate.moderatorId,
@@ -1115,73 +1110,75 @@ function attachDebate(ctx) {
1115
1110
 
1116
1111
  // --- User interaction during debate ---
1117
1112
 
1118
- function handleDebateComment(ws, msg) {
1113
+ function handleDebateHandRaise(ws) {
1119
1114
  var session = ctx.getSessionForWs(ws);
1120
1115
  if (!session) return;
1121
1116
 
1122
1117
  var debate = session._debate;
1123
- if (!debate || debate.phase !== "live") {
1124
- ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
1125
- return;
1126
- }
1127
-
1128
- // If awaiting conclude confirmation, re-send the confirm prompt instead
1118
+ if (!debate || debate.phase !== "live") return;
1119
+ if (debate.awaitingUserFloor || debate.handRaised) return;
1129
1120
  if (debate.awaitingConcludeConfirm) {
1130
1121
  ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
1131
1122
  return;
1132
1123
  }
1133
1124
 
1125
+ debate.handRaised = true;
1126
+ ctx.sendToSession(session.localId, { type: "debate_hand_raised" });
1127
+
1128
+ // If no one is speaking, yield floor immediately
1129
+ if (!debate.turnInProgress) {
1130
+ yieldFloorToUser(session);
1131
+ }
1132
+ // Otherwise: current speaker finishes -> handRaised detected -> yieldFloorToUser
1133
+ }
1134
+
1135
+ function handleDebateComment(ws, msg) {
1136
+ // Legacy: kept for compatibility but now hand raise is separate
1137
+ var session = ctx.getSessionForWs(ws);
1138
+ if (!session) return;
1139
+
1140
+ var debate = session._debate;
1141
+ if (!debate || debate.phase !== "live") {
1142
+ ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
1143
+ return;
1144
+ }
1145
+ if (debate.awaitingUserFloor) return;
1134
1146
  if (!msg.text) return;
1135
1147
 
1136
1148
  debate.pendingComment = { text: msg.text };
1149
+ debate.handRaised = true;
1137
1150
  ctx.sendToSession(session.localId, { type: "debate_comment_queued", text: msg.text });
1138
1151
 
1139
- // If a panelist turn is in progress, abort it and go straight to moderator
1140
- if (debate.turnInProgress && debate._currentTurnMateId && debate._currentTurnMateId !== debate.moderatorId) {
1141
- var abortMateId = debate._currentTurnMateId;
1142
- console.log("[debate] User raised hand during panelist turn, aborting " + abortMateId);
1143
-
1144
- // Close the panelist's mention session to stop generation
1145
- if (debate.panelistSessions[abortMateId]) {
1146
- try { debate.panelistSessions[abortMateId].close(); } catch (e) {}
1147
- delete debate.panelistSessions[abortMateId];
1148
- }
1152
+ if (!debate.turnInProgress) {
1153
+ injectUserComment(session);
1154
+ }
1155
+ }
1149
1156
 
1150
- // Save partial text as interrupted turn
1151
- var partialText = debate._currentTurnText || "(interrupted by audience)";
1152
- var profile = ctx.getMateProfile(debate.mateCtx, abortMateId);
1153
- var panelistInfo = null;
1154
- for (var pi = 0; pi < debate.panelists.length; pi++) {
1155
- if (debate.panelists[pi].mateId === abortMateId) { panelistInfo = debate.panelists[pi]; break; }
1156
- }
1157
+ function yieldFloorToUser(session) {
1158
+ var debate = session._debate;
1159
+ if (!debate || !debate.moderatorSession || debate.phase === "ended") return;
1157
1160
 
1158
- ctx.sendToSession(session.localId, {
1159
- type: "debate_turn_done",
1160
- mateId: abortMateId,
1161
- mateName: profile.name,
1162
- role: panelistInfo ? panelistInfo.role : "",
1163
- text: partialText,
1164
- interrupted: true,
1165
- avatarStyle: profile.avatarStyle,
1166
- avatarSeed: profile.avatarSeed,
1167
- avatarColor: profile.avatarColor,
1168
- });
1161
+ debate.handRaised = false;
1162
+ debate.turnInProgress = true;
1163
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1169
1164
 
1170
- var turnEntry = { type: "debate_turn_done", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", round: debate.round, text: partialText, avatarStyle: profile.avatarStyle, avatarSeed: profile.avatarSeed, avatarColor: profile.avatarColor, interrupted: true };
1171
- session.history.push(turnEntry);
1172
- ctx.sm.appendToSessionFile(session, turnEntry);
1173
- debate.history.push({ speaker: "panelist", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: partialText });
1165
+ ctx.sendToSession(session.localId, {
1166
+ type: "debate_turn",
1167
+ mateId: debate.moderatorId,
1168
+ mateName: moderatorProfile.name,
1169
+ role: "moderator",
1170
+ round: debate.round,
1171
+ avatarColor: moderatorProfile.avatarColor,
1172
+ avatarStyle: moderatorProfile.avatarStyle,
1173
+ avatarSeed: moderatorProfile.avatarSeed,
1174
+ });
1174
1175
 
1175
- debate.turnInProgress = false;
1176
- debate._currentTurnMateId = null;
1177
- debate._currentTurnText = "";
1178
- }
1176
+ var feedText = "[The user raised their hand to speak.]\n" +
1177
+ "[Acknowledge this briefly and yield the floor to the user. Say something like " +
1178
+ "\"Go ahead\" or \"The floor is yours\". Do NOT call on any panelist (no @mentions). " +
1179
+ "The debate will pause for the user to speak.]";
1179
1180
 
1180
- // Inject to moderator immediately if no turn in progress (or just aborted)
1181
- if (!debate.turnInProgress) {
1182
- injectUserComment(session);
1183
- }
1184
- // If moderator is currently speaking, pendingComment will be picked up after moderator's onDone
1181
+ debate.moderatorSession.pushMessage(feedText, buildModeratorYieldCallbacks(session));
1185
1182
  }
1186
1183
 
1187
1184
  function injectUserComment(session) {
@@ -1190,6 +1187,7 @@ function attachDebate(ctx) {
1190
1187
 
1191
1188
  var comment = debate.pendingComment;
1192
1189
  debate.pendingComment = null;
1190
+ debate.handRaised = false;
1193
1191
 
1194
1192
  // Record in debate history
1195
1193
  debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: comment.text });
@@ -1199,7 +1197,7 @@ function attachDebate(ctx) {
1199
1197
  ctx.sm.appendToSessionFile(session, commentEntry);
1200
1198
  ctx.sendToSession(session.localId, commentEntry);
1201
1199
 
1202
- // Feed to moderator
1200
+ // Feed to moderator: yield the floor to the user
1203
1201
  debate.turnInProgress = true;
1204
1202
  var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1205
1203
 
@@ -1216,9 +1214,90 @@ function attachDebate(ctx) {
1216
1214
 
1217
1215
  var feedText = "[The user raised their hand and said:]\n" +
1218
1216
  comment.text + "\n" +
1219
- "[Please acknowledge this and weave it into the discussion. Then continue the debate.]";
1217
+ "[Acknowledge the user's input. Briefly respond, then YIELD THE FLOOR to the user by saying something like " +
1218
+ "\"The floor is yours\" or \"Go ahead\". Do NOT call on any panelist (no @mentions). " +
1219
+ "The debate will pause for the user to speak.]";
1220
+
1221
+ debate.moderatorSession.pushMessage(feedText, buildModeratorYieldCallbacks(session));
1222
+ }
1223
+
1224
+ function buildModeratorYieldCallbacks(session) {
1225
+ var debate = session._debate;
1226
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1227
+ return {
1228
+ onActivity: function (activity) {
1229
+ if (session._debate && session._debate.phase !== "ended") {
1230
+ ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1231
+ }
1232
+ },
1233
+ onDelta: function (delta) {
1234
+ if (session._debate && session._debate.phase !== "ended") {
1235
+ ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
1236
+ }
1237
+ },
1238
+ onDone: function (fullText) {
1239
+ if (!debate || debate.phase === "ended") return;
1240
+ debate.turnInProgress = false;
1241
+
1242
+ // Record moderator yield turn
1243
+ debate.history.push({ speaker: "moderator", mateId: debate.moderatorId, mateName: moderatorProfile.name, text: fullText });
1244
+ var turnEntry = { type: "debate_turn_done", mateId: debate.moderatorId, mateName: moderatorProfile.name, role: "moderator", round: debate.round, text: fullText, avatarStyle: moderatorProfile.avatarStyle, avatarSeed: moderatorProfile.avatarSeed, avatarColor: moderatorProfile.avatarColor };
1245
+ session.history.push(turnEntry);
1246
+ ctx.sm.appendToSessionFile(session, turnEntry);
1247
+ ctx.sendToSession(session.localId, turnEntry);
1248
+
1249
+ // Enter user floor mode: pause debate and show input
1250
+ debate.awaitingUserFloor = true;
1251
+ persistDebateState(session);
1252
+ ctx.sendToSession(session.localId, { type: "debate_user_floor", topic: debate.topic, round: debate.round });
1253
+ },
1254
+ onError: function (errMsg) {
1255
+ console.error("[debate] Moderator yield error:", errMsg);
1256
+ endDebate(session, "error");
1257
+ },
1258
+ };
1259
+ }
1260
+
1261
+ function handleDebateUserFloorResponse(ws, msg) {
1262
+ var session = ctx.getSessionForWs(ws);
1263
+ if (!session) return;
1264
+
1265
+ var debate = session._debate;
1266
+ if (!debate || !debate.awaitingUserFloor || debate.phase !== "live") return;
1267
+
1268
+ debate.awaitingUserFloor = false;
1269
+ var userText = (msg && msg.text) ? msg.text.trim() : "";
1270
+ if (!userText) return;
1271
+
1272
+ // Record user's floor contribution
1273
+ debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: userText });
1274
+ var floorEntry = { type: "debate_user_floor_done", text: userText };
1275
+ session.history.push(floorEntry);
1276
+ ctx.sm.appendToSessionFile(session, floorEntry);
1277
+ ctx.sendToSession(session.localId, floorEntry);
1278
+
1279
+ // Feed to moderator to resume debate
1280
+ debate.turnInProgress = true;
1281
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1282
+
1283
+ debateMateProcessing(debate.moderatorId, true);
1284
+ ctx.sendToSession(session.localId, {
1285
+ type: "debate_turn",
1286
+ mateId: debate.moderatorId,
1287
+ mateName: moderatorProfile.name,
1288
+ role: "moderator",
1289
+ round: debate.round,
1290
+ avatarColor: moderatorProfile.avatarColor,
1291
+ avatarStyle: moderatorProfile.avatarStyle,
1292
+ avatarSeed: moderatorProfile.avatarSeed,
1293
+ });
1294
+
1295
+ var feedText = "[The user took the floor and said:]\n" +
1296
+ userText + "\n" +
1297
+ "[Acknowledge the user's contribution and resume the debate. Call on the next panelist with @TheirName.]";
1220
1298
 
1221
1299
  debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
1300
+ persistDebateState(session);
1222
1301
  }
1223
1302
 
1224
1303
  function handleDebateConfirmBrief(ws) {
@@ -1391,6 +1470,7 @@ function attachDebate(ctx) {
1391
1470
  ctx.sendToSession(session.localId, resumedMsg);
1392
1471
 
1393
1472
  debate.turnInProgress = true;
1473
+ debateMateProcessing(debate.moderatorId, true);
1394
1474
  ctx.sendToSession(session.localId, {
1395
1475
  type: "debate_turn",
1396
1476
  mateId: debate.moderatorId,
@@ -1402,9 +1482,15 @@ function attachDebate(ctx) {
1402
1482
  avatarSeed: moderatorProfile.avatarSeed,
1403
1483
  });
1404
1484
 
1485
+ // Build explicit panelist list so the moderator knows who to call on
1486
+ var panelistNames = debate.panelists.map(function (p) {
1487
+ var prof = ctx.getMateProfile(mateCtx, p.mateId);
1488
+ return "@" + prof.name;
1489
+ });
1490
+ var panelistList = panelistNames.join(", ");
1405
1491
  var resumePrompt = instruction
1406
- ? "[The audience has requested the debate continue with the following direction]\nUser: " + instruction + "\n\n[As moderator, acknowledge this input and call on a panelist with @TheirName to continue the discussion.]"
1407
- : "[The audience has requested the debate continue. Call on the next panelist with @TheirName to explore additional perspectives.]";
1492
+ ? "[SYSTEM: The audience has requested the debate continue with new direction. You MUST call on a panelist to continue. Available panelists: " + panelistList + "]\n\nUser direction: " + instruction + "\n\n[Acknowledge this input briefly, then call on a panelist by writing their @Name to continue the discussion on this new direction. You must @mention exactly one panelist.]"
1493
+ : "[SYSTEM: The audience has requested the debate continue. You MUST call on the next panelist. Available panelists: " + panelistList + "]\n\n[Call on a panelist by writing their @Name to explore additional perspectives. You must @mention exactly one panelist.]";
1408
1494
 
1409
1495
  // If resuming from ended state, moderator session may be dead. Create a new one.
1410
1496
  if (wasEnded || !debate.moderatorSession || !debate.moderatorSession.isAlive()) {
@@ -1414,7 +1500,8 @@ function attachDebate(ctx) {
1414
1500
  var moderatorContext = buildModeratorContext(debate) + digests;
1415
1501
 
1416
1502
  // Include debate history so moderator has context
1417
- moderatorContext += "\n\nDebate history so far:\n---\n";
1503
+ moderatorContext += "\n\nIMPORTANT: This debate was previously paused and is now being RESUMED. You must continue the debate by calling on a panelist with @TheirName. Do NOT conclude or summarize.\n";
1504
+ moderatorContext += "\nDebate history so far:\n---\n";
1418
1505
  for (var hi = 0; hi < debate.history.length; hi++) {
1419
1506
  var h = debate.history[hi];
1420
1507
  moderatorContext += (h.mateName || h.speaker || "Unknown") + " (" + (h.role || "") + "): " + (h.text || "").slice(0, 500) + "\n\n";
@@ -1464,6 +1551,12 @@ function attachDebate(ctx) {
1464
1551
  var debate = session._debate;
1465
1552
  if (!debate || debate.phase === "ended") return;
1466
1553
 
1554
+ // Clear all mate strip processing dots
1555
+ debateMateProcessing(debate.moderatorId, false);
1556
+ for (var ei = 0; ei < debate.panelists.length; ei++) {
1557
+ debateMateProcessing(debate.panelists[ei].mateId, false);
1558
+ }
1559
+
1467
1560
  debate.phase = "ended";
1468
1561
  debate.turnInProgress = false;
1469
1562
  persistDebateState(session);
@@ -1588,16 +1681,61 @@ function attachDebate(ctx) {
1588
1681
  })();
1589
1682
  }
1590
1683
 
1684
+ // --- MCP-based debate proposal approval ---
1685
+
1686
+ function handleMcpDebateApproval(session, briefData, mateId, ws) {
1687
+ if (session._debate && (session._debate.phase === "preparing" || session._debate.phase === "reviewing" || session._debate.phase === "live")) {
1688
+ console.warn("[debate] Cannot start MCP debate: another debate is active on this session");
1689
+ return;
1690
+ }
1691
+
1692
+ var userId = ws && ws._clayUser ? ws._clayUser.id : (session.ownerId || ctx.projectOwnerId || null);
1693
+ var mateCtx = matesModule.buildMateCtx(userId);
1694
+ var debateId = "debate_" + Date.now();
1695
+
1696
+ var debate = {
1697
+ phase: "reviewing",
1698
+ topic: briefData.topic || "Untitled debate",
1699
+ format: briefData.format || "free_discussion",
1700
+ context: briefData.context || "",
1701
+ specialRequests: briefData.specialRequests || null,
1702
+ moderatorId: mateId,
1703
+ panelists: (briefData.panelists || []).map(function (p) {
1704
+ return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
1705
+ }),
1706
+ mateCtx: mateCtx,
1707
+ moderatorSession: null,
1708
+ panelistSessions: {},
1709
+ nameMap: null,
1710
+ turnInProgress: false,
1711
+ pendingComment: null,
1712
+ round: 1,
1713
+ history: [],
1714
+ setupSessionId: null,
1715
+ debateId: debateId,
1716
+ briefPath: null,
1717
+ ownerId: userId,
1718
+ };
1719
+ debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
1720
+ session._debate = debate;
1721
+
1722
+ console.log("[debate] MCP debate approved. Topic:", debate.topic, "debateId:", debateId);
1723
+ startDebateLive(session);
1724
+ }
1725
+
1591
1726
  // --- Public API ---
1592
1727
 
1593
1728
  return {
1594
1729
  handleDebateStart: handleDebateStart,
1730
+ handleDebateHandRaise: handleDebateHandRaise,
1595
1731
  handleDebateComment: handleDebateComment,
1596
1732
  handleDebateStop: handleDebateStop,
1597
1733
  handleDebateConcludeResponse: handleDebateConcludeResponse,
1598
1734
  handleDebateConfirmBrief: handleDebateConfirmBrief,
1735
+ handleDebateUserFloorResponse: handleDebateUserFloorResponse,
1599
1736
  restoreDebateState: restoreDebateState,
1600
1737
  checkForDmDebateBrief: checkForDmDebateBrief,
1738
+ handleMcpDebateApproval: handleMcpDebateApproval,
1601
1739
  };
1602
1740
  }
1603
1741