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.
- package/lib/browser-mcp-server.js +4 -4
- package/lib/daemon.js +1 -1
- package/lib/debate-mcp-server.js +94 -0
- package/lib/mates.js +12 -24
- package/lib/project-debate.js +304 -166
- package/lib/project-mate-interaction.js +10 -5
- package/lib/project.js +128 -39
- package/lib/public/app.js +452 -127
- package/lib/public/css/debate.css +230 -2
- package/lib/public/css/filebrowser.css +41 -4
- package/lib/public/css/icon-strip.css +10 -10
- package/lib/public/css/input.css +107 -19
- package/lib/public/css/mates.css +56 -57
- package/lib/public/css/mention.css +7 -4
- package/lib/public/css/messages.css +17 -0
- package/lib/public/css/mobile-nav.css +3 -1
- package/lib/public/css/rewind.css +17 -4
- package/lib/public/index.html +23 -15
- package/lib/public/modules/context-sources.js +21 -6
- package/lib/public/modules/debate.js +298 -97
- package/lib/public/modules/input.js +18 -1
- package/lib/public/modules/mate-knowledge.js +11 -11
- package/lib/public/modules/mate-memory.js +5 -5
- package/lib/public/modules/mate-sidebar.js +13 -9
- package/lib/public/modules/mention.js +40 -2
- package/lib/public/modules/sidebar.js +105 -26
- package/lib/public/modules/terminal.js +62 -6
- package/lib/public/modules/theme.js +2 -1
- package/lib/public/modules/tools.js +47 -23
- package/lib/sdk-bridge.js +134 -21
- package/lib/sessions.js +2 -2
- package/package.json +1 -1
package/lib/project-debate.js
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
269
|
+
// Replace panelists with those selected in the brief
|
|
259
270
|
if (brief.panelists && brief.panelists.length) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
//
|
|
276
|
-
if (!debate.setupSessionId) {
|
|
277
|
-
console.log("[debate] Brief picked up
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
504
|
-
ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
727
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
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
|
-
"[
|
|
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
|
|
1407
|
-
: "[The audience has requested the debate continue.
|
|
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\
|
|
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
|
|