clay-server 2.23.0 → 2.23.1-beta.1
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/project-debate.js +1616 -0
- package/lib/project.js +38 -1567
- package/lib/public/app.js +2 -1
- package/lib/public/css/debate.css +48 -0
- package/lib/public/modules/debate.js +41 -0
- package/lib/sdk-bridge.js +43 -9
- package/lib/sdk-worker.js +16 -10
- package/package.json +1 -1
|
@@ -0,0 +1,1616 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
4
|
+
var matesModule = require("./mates");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attach debate engine to a project context.
|
|
8
|
+
*
|
|
9
|
+
* ctx fields:
|
|
10
|
+
* cwd, slug, send, sendTo, sendToSession, sm, sdk,
|
|
11
|
+
* getMateProfile, loadMateClaudeMd, loadMateDigests,
|
|
12
|
+
* hydrateImageRefs, onProcessingChanged, getLinuxUserForSession, getSessionForWs,
|
|
13
|
+
* updateMemorySummary, initMemorySummary, enqueueDigest
|
|
14
|
+
*/
|
|
15
|
+
function attachDebate(ctx) {
|
|
16
|
+
|
|
17
|
+
// --- Helpers shared with other modules ---
|
|
18
|
+
|
|
19
|
+
function escapeRegex(str) {
|
|
20
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildDebateNameMap(panelists, mateCtx) {
|
|
24
|
+
var nameMap = {};
|
|
25
|
+
for (var i = 0; i < panelists.length; i++) {
|
|
26
|
+
var mate = matesModule.getMate(mateCtx, panelists[i].mateId);
|
|
27
|
+
if (!mate) continue;
|
|
28
|
+
var name = (mate.profile && mate.profile.displayName) || mate.name || "";
|
|
29
|
+
if (name) {
|
|
30
|
+
nameMap[name] = panelists[i].mateId;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return nameMap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectMentions(text, nameMap) {
|
|
37
|
+
var names = Object.keys(nameMap);
|
|
38
|
+
// Sort by length descending to match longest name first
|
|
39
|
+
names.sort(function (a, b) { return b.length - a.length; });
|
|
40
|
+
var mentioned = [];
|
|
41
|
+
// Strip markdown inline formatting so **@Name**, ~~@Name~~, `@Name`, [@Name](url) etc. still match
|
|
42
|
+
var cleaned = text
|
|
43
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) -> text
|
|
44
|
+
.replace(/`([^`]*)`/g, "$1") // `code` -> code
|
|
45
|
+
.replace(/(\*{1,3}|_{1,3}|~{2})/g, ""); // bold, italic, strikethrough markers
|
|
46
|
+
console.log("[debate-mention] nameMap keys:", JSON.stringify(names));
|
|
47
|
+
console.log("[debate-mention] text snippet:", cleaned.slice(0, 200));
|
|
48
|
+
for (var i = 0; i < names.length; i++) {
|
|
49
|
+
// Match @Name followed by any non-name character (not alphanumeric, not Korean, not dash/underscore)
|
|
50
|
+
var pattern = new RegExp("@" + escapeRegex(names[i]) + "(?![\\p{L}\\p{N}_-])", "iu");
|
|
51
|
+
var matched = pattern.test(cleaned);
|
|
52
|
+
console.log("[debate-mention] testing @" + names[i] + " pattern=" + pattern.toString() + " matched=" + matched);
|
|
53
|
+
if (matched) {
|
|
54
|
+
var mateId = nameMap[names[i]];
|
|
55
|
+
if (mentioned.indexOf(mateId) === -1) {
|
|
56
|
+
mentioned.push(mateId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return mentioned;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Context builders ---
|
|
64
|
+
|
|
65
|
+
function buildModeratorContext(debate) {
|
|
66
|
+
var lines = [
|
|
67
|
+
"You are moderating a structured debate among your AI teammates.",
|
|
68
|
+
"",
|
|
69
|
+
"Topic: " + debate.topic,
|
|
70
|
+
"Format: " + debate.format,
|
|
71
|
+
"Context: " + debate.context,
|
|
72
|
+
];
|
|
73
|
+
if (debate.specialRequests) {
|
|
74
|
+
lines.push("Special requests: " + debate.specialRequests);
|
|
75
|
+
}
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("Panelists:");
|
|
78
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
79
|
+
var p = debate.panelists[i];
|
|
80
|
+
var profile = ctx.getMateProfile(debate.mateCtx, p.mateId);
|
|
81
|
+
lines.push("- @" + profile.name + " (" + p.role + "): " + p.brief);
|
|
82
|
+
}
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("RULES:");
|
|
85
|
+
lines.push("1. To call on a panelist, mention them with @TheirName in your response.");
|
|
86
|
+
lines.push("2. Only mention ONE panelist per response. Wait for their answer before calling the next.");
|
|
87
|
+
lines.push("3. When you mention a panelist, clearly state what you want them to address.");
|
|
88
|
+
lines.push("4. After hearing from all panelists, you may start additional rounds.");
|
|
89
|
+
lines.push("5. When you believe the debate has reached a natural conclusion, provide a summary WITHOUT mentioning any panelist. A response with no @mention signals the end of the debate.");
|
|
90
|
+
lines.push("6. If the user interjects with a comment, acknowledge it and weave it into the discussion.");
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push("Begin by introducing the topic and calling on the first panelist.");
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildPanelistContext(debate, panelistInfo) {
|
|
97
|
+
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
98
|
+
var lines = [
|
|
99
|
+
"You are participating in a structured debate as a panelist.",
|
|
100
|
+
"",
|
|
101
|
+
"Topic: " + debate.topic,
|
|
102
|
+
"Your role: " + panelistInfo.role,
|
|
103
|
+
"Your brief: " + panelistInfo.brief,
|
|
104
|
+
"",
|
|
105
|
+
"Other panelists:",
|
|
106
|
+
];
|
|
107
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
108
|
+
var p = debate.panelists[i];
|
|
109
|
+
if (p.mateId === panelistInfo.mateId) continue;
|
|
110
|
+
var profile = ctx.getMateProfile(debate.mateCtx, p.mateId);
|
|
111
|
+
lines.push("- @" + profile.name + " (" + p.role + "): " + p.brief);
|
|
112
|
+
}
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push("The moderator is @" + moderatorProfile.name + ". They will call on you when it is your turn.");
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push("RULES:");
|
|
117
|
+
lines.push("1. Stay in your assigned role and perspective.");
|
|
118
|
+
lines.push("2. Respond to the specific question or prompt from the moderator.");
|
|
119
|
+
lines.push("3. You may reference what other panelists have said.");
|
|
120
|
+
lines.push("4. Keep responses focused and substantive. Do not ramble.");
|
|
121
|
+
lines.push("5. You have read-only access to project files if needed to support your arguments.");
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildDebateToolHandler(session) {
|
|
126
|
+
return function (toolName, input, toolOpts) {
|
|
127
|
+
var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
128
|
+
if (autoAllow[toolName]) {
|
|
129
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
130
|
+
}
|
|
131
|
+
return Promise.resolve({
|
|
132
|
+
behavior: "deny",
|
|
133
|
+
message: "Read-only access during debate. You cannot make changes.",
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- State persistence ---
|
|
139
|
+
|
|
140
|
+
function persistDebateState(session) {
|
|
141
|
+
if (!session._debate) return;
|
|
142
|
+
var d = session._debate;
|
|
143
|
+
session.debateState = {
|
|
144
|
+
phase: d.phase,
|
|
145
|
+
topic: d.topic,
|
|
146
|
+
format: d.format,
|
|
147
|
+
context: d.context || "",
|
|
148
|
+
specialRequests: d.specialRequests || null,
|
|
149
|
+
moderatorId: d.moderatorId,
|
|
150
|
+
panelists: d.panelists.map(function (p) {
|
|
151
|
+
return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
|
|
152
|
+
}),
|
|
153
|
+
briefPath: d.briefPath || null,
|
|
154
|
+
debateId: d.debateId || null,
|
|
155
|
+
setupSessionId: d.setupSessionId || null,
|
|
156
|
+
setupStartedAt: d.setupStartedAt || null,
|
|
157
|
+
round: d.round || 1,
|
|
158
|
+
awaitingConcludeConfirm: !!d.awaitingConcludeConfirm,
|
|
159
|
+
};
|
|
160
|
+
ctx.sm.saveSessionFile(session);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function restoreDebateFromState(session) {
|
|
164
|
+
var ds = session.debateState;
|
|
165
|
+
if (!ds) return null;
|
|
166
|
+
var userId = null; // Will be set when WS connects
|
|
167
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
168
|
+
var debate = {
|
|
169
|
+
phase: ds.phase,
|
|
170
|
+
topic: ds.topic,
|
|
171
|
+
format: ds.format,
|
|
172
|
+
context: ds.context || "",
|
|
173
|
+
specialRequests: ds.specialRequests || null,
|
|
174
|
+
moderatorId: ds.moderatorId,
|
|
175
|
+
panelists: ds.panelists || [],
|
|
176
|
+
mateCtx: mateCtx,
|
|
177
|
+
moderatorSession: null,
|
|
178
|
+
panelistSessions: {},
|
|
179
|
+
nameMap: buildDebateNameMap(ds.panelists || [], mateCtx),
|
|
180
|
+
turnInProgress: false,
|
|
181
|
+
pendingComment: null,
|
|
182
|
+
round: ds.round || 1,
|
|
183
|
+
history: [],
|
|
184
|
+
setupSessionId: ds.setupSessionId || null,
|
|
185
|
+
debateId: ds.debateId || null,
|
|
186
|
+
setupStartedAt: ds.setupStartedAt || null,
|
|
187
|
+
briefPath: ds.briefPath || null,
|
|
188
|
+
awaitingConcludeConfirm: !!ds.awaitingConcludeConfirm,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Fallback: if awaitingConcludeConfirm was not persisted, detect from history
|
|
192
|
+
if (!debate.awaitingConcludeConfirm && ds.phase === "live") {
|
|
193
|
+
var hasEnded = false;
|
|
194
|
+
var hasConclude = false;
|
|
195
|
+
var lastModText = null;
|
|
196
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
197
|
+
var h = session.history[i];
|
|
198
|
+
if (h.type === "debate_ended") hasEnded = true;
|
|
199
|
+
if (h.type === "debate_conclude_confirm") hasConclude = true;
|
|
200
|
+
if (h.type === "debate_turn_done" && h.role === "moderator") lastModText = h.text || "";
|
|
201
|
+
}
|
|
202
|
+
// conclude_confirm in history without a subsequent ended = still awaiting user decision
|
|
203
|
+
if (hasConclude && !hasEnded) {
|
|
204
|
+
debate.awaitingConcludeConfirm = true;
|
|
205
|
+
} else if (!hasEnded && !hasConclude && lastModText !== null) {
|
|
206
|
+
// No explicit entry yet; infer from last moderator text having no @mentions
|
|
207
|
+
var mentions = detectMentions(lastModText, debate.nameMap);
|
|
208
|
+
if (mentions.length === 0) {
|
|
209
|
+
debate.awaitingConcludeConfirm = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
session._debate = debate;
|
|
215
|
+
return debate;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Brief watcher ---
|
|
219
|
+
|
|
220
|
+
function startDebateBriefWatcher(session, debate, briefPath) {
|
|
221
|
+
if (!briefPath) {
|
|
222
|
+
console.error("[debate] No briefPath provided to watcher");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// Persist briefPath on debate so restoration can reuse it
|
|
226
|
+
debate.briefPath = briefPath;
|
|
227
|
+
var watchDir = path.dirname(briefPath);
|
|
228
|
+
var briefFilename = path.basename(briefPath);
|
|
229
|
+
|
|
230
|
+
// Clean up any existing watcher
|
|
231
|
+
if (debate._briefWatcher) {
|
|
232
|
+
try { debate._briefWatcher.close(); } catch (e) {}
|
|
233
|
+
debate._briefWatcher = null;
|
|
234
|
+
}
|
|
235
|
+
if (debate._briefDebounce) {
|
|
236
|
+
clearTimeout(debate._briefDebounce);
|
|
237
|
+
debate._briefDebounce = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function checkDebateBrief() {
|
|
241
|
+
try {
|
|
242
|
+
var raw = fs.readFileSync(briefPath, "utf8");
|
|
243
|
+
var brief = JSON.parse(raw);
|
|
244
|
+
|
|
245
|
+
// Stop watching
|
|
246
|
+
if (debate._briefWatcher) { debate._briefWatcher.close(); debate._briefWatcher = null; }
|
|
247
|
+
if (debate._briefDebounce) { clearTimeout(debate._briefDebounce); debate._briefDebounce = null; }
|
|
248
|
+
|
|
249
|
+
// Clean up the brief file
|
|
250
|
+
try { fs.unlinkSync(briefPath); } catch (e) {}
|
|
251
|
+
|
|
252
|
+
// Apply brief to debate state
|
|
253
|
+
debate.topic = brief.topic || debate.topic;
|
|
254
|
+
debate.format = brief.format || debate.format;
|
|
255
|
+
debate.context = brief.context || "";
|
|
256
|
+
debate.specialRequests = brief.specialRequests || null;
|
|
257
|
+
|
|
258
|
+
// Update panelists with roles from the brief
|
|
259
|
+
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
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Rebuild name map with updated roles
|
|
272
|
+
var mateCtx = debate.mateCtx || matesModule.buildMateCtx(null);
|
|
273
|
+
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
274
|
+
|
|
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);
|
|
278
|
+
debate.phase = "reviewing";
|
|
279
|
+
persistDebateState(session);
|
|
280
|
+
|
|
281
|
+
var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
|
|
282
|
+
var briefReadyMsg = {
|
|
283
|
+
type: "debate_brief_ready",
|
|
284
|
+
debateId: debate.debateId,
|
|
285
|
+
topic: debate.topic,
|
|
286
|
+
format: debate.format || "free_discussion",
|
|
287
|
+
context: debate.context || "",
|
|
288
|
+
specialRequests: debate.specialRequests || null,
|
|
289
|
+
moderatorId: debate.moderatorId,
|
|
290
|
+
moderatorName: moderatorProfile.name,
|
|
291
|
+
panelists: debate.panelists.map(function (p) {
|
|
292
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
293
|
+
return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
ctx.sendToSession(session.localId, briefReadyMsg);
|
|
297
|
+
} else {
|
|
298
|
+
console.log("[debate] Brief picked up, transitioning to live. Topic:", debate.topic);
|
|
299
|
+
// Transition to live (standard flow via modal/skill)
|
|
300
|
+
startDebateLive(session);
|
|
301
|
+
}
|
|
302
|
+
} catch (e) {
|
|
303
|
+
// File not ready yet or invalid JSON, keep watching
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
try { fs.mkdirSync(watchDir, { recursive: true }); } catch (e) {}
|
|
309
|
+
debate._briefWatcher = fs.watch(watchDir, function (eventType, filename) {
|
|
310
|
+
if (filename === briefFilename) {
|
|
311
|
+
if (debate._briefDebounce) clearTimeout(debate._briefDebounce);
|
|
312
|
+
debate._briefDebounce = setTimeout(checkDebateBrief, 300);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
debate._briefWatcher.on("error", function () {});
|
|
316
|
+
console.log("[debate] Watching for " + briefFilename + " at " + watchDir);
|
|
317
|
+
} catch (e) {
|
|
318
|
+
console.error("[debate] Failed to watch " + watchDir + ":", e.message);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check immediately in case the file already exists (server restart scenario)
|
|
322
|
+
checkDebateBrief();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Restore debate on reconnect ---
|
|
326
|
+
|
|
327
|
+
function restoreDebateState(ws) {
|
|
328
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
329
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
330
|
+
|
|
331
|
+
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
|
+
// Send preparing sticky to the connected client
|
|
362
|
+
ctx.sendTo(ws, {
|
|
363
|
+
type: "debate_preparing",
|
|
364
|
+
topic: debate.topic,
|
|
365
|
+
moderatorId: debate.moderatorId,
|
|
366
|
+
moderatorName: moderatorProfile.name,
|
|
367
|
+
setupSessionId: debate.setupSessionId,
|
|
368
|
+
panelists: debate.panelists.map(function (p) {
|
|
369
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
370
|
+
return { mateId: p.mateId, name: prof.name };
|
|
371
|
+
}),
|
|
372
|
+
});
|
|
373
|
+
} else if (phase === "reviewing") {
|
|
374
|
+
console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
|
|
375
|
+
ctx.sendTo(ws, {
|
|
376
|
+
type: "debate_brief_ready",
|
|
377
|
+
debateId: debate.debateId,
|
|
378
|
+
topic: debate.topic,
|
|
379
|
+
format: debate.format || "free_discussion",
|
|
380
|
+
context: debate.context || "",
|
|
381
|
+
specialRequests: debate.specialRequests || null,
|
|
382
|
+
moderatorId: debate.moderatorId,
|
|
383
|
+
moderatorName: moderatorProfile.name,
|
|
384
|
+
panelists: debate.panelists.map(function (p) {
|
|
385
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
386
|
+
return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
} else if (phase === "live") {
|
|
390
|
+
console.log("[debate] Restoring debate (live). topic:", debate.topic, "awaitingConclude:", debate.awaitingConcludeConfirm);
|
|
391
|
+
// Debate was live when server restarted. It can't resume AI turns,
|
|
392
|
+
// but we can show the sticky and let user see history.
|
|
393
|
+
ctx.sendTo(ws, {
|
|
394
|
+
type: "debate_started",
|
|
395
|
+
topic: debate.topic,
|
|
396
|
+
format: debate.format,
|
|
397
|
+
round: debate.round,
|
|
398
|
+
moderatorId: debate.moderatorId,
|
|
399
|
+
moderatorName: moderatorProfile.name,
|
|
400
|
+
panelists: debate.panelists.map(function (p) {
|
|
401
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
402
|
+
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
// If moderator had concluded, re-send conclude confirm so client shows End/Continue UI
|
|
406
|
+
if (debate.awaitingConcludeConfirm) {
|
|
407
|
+
ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- Check for DM debate brief ---
|
|
414
|
+
|
|
415
|
+
function checkForDmDebateBrief(session, mateId, mateCtx) {
|
|
416
|
+
// Skip if there's already an active debate on this session
|
|
417
|
+
if (session._debate && (session._debate.phase === "preparing" || session._debate.phase === "reviewing" || session._debate.phase === "live")) return;
|
|
418
|
+
|
|
419
|
+
var debatesDir = path.join(ctx.cwd, ".clay", "debates");
|
|
420
|
+
var dirs;
|
|
421
|
+
try {
|
|
422
|
+
dirs = fs.readdirSync(debatesDir);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return; // No debates directory
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (var i = 0; i < dirs.length; i++) {
|
|
428
|
+
var briefPath = path.join(debatesDir, dirs[i], "brief.json");
|
|
429
|
+
var raw;
|
|
430
|
+
try {
|
|
431
|
+
raw = fs.readFileSync(briefPath, "utf8");
|
|
432
|
+
} catch (e) {
|
|
433
|
+
continue; // No brief.json in this dir
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
var brief;
|
|
437
|
+
try {
|
|
438
|
+
brief = JSON.parse(raw);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
continue; // Invalid JSON
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Found a valid brief - create debate state
|
|
444
|
+
var debateId = dirs[i];
|
|
445
|
+
console.log("[debate] Found DM debate brief from mate " + mateId + ", debateId:", debateId);
|
|
446
|
+
|
|
447
|
+
// Clean up the brief file
|
|
448
|
+
try { fs.unlinkSync(briefPath); } catch (e) {}
|
|
449
|
+
|
|
450
|
+
var debate = {
|
|
451
|
+
phase: "reviewing",
|
|
452
|
+
topic: brief.topic || "Untitled debate",
|
|
453
|
+
format: brief.format || "free_discussion",
|
|
454
|
+
context: brief.context || "",
|
|
455
|
+
specialRequests: brief.specialRequests || null,
|
|
456
|
+
moderatorId: mateId,
|
|
457
|
+
panelists: (brief.panelists || []).map(function (p) {
|
|
458
|
+
return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
|
|
459
|
+
}),
|
|
460
|
+
mateCtx: mateCtx,
|
|
461
|
+
moderatorSession: null,
|
|
462
|
+
panelistSessions: {},
|
|
463
|
+
nameMap: null,
|
|
464
|
+
turnInProgress: false,
|
|
465
|
+
pendingComment: null,
|
|
466
|
+
round: 1,
|
|
467
|
+
history: [],
|
|
468
|
+
setupSessionId: null,
|
|
469
|
+
debateId: debateId,
|
|
470
|
+
briefPath: briefPath,
|
|
471
|
+
};
|
|
472
|
+
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
473
|
+
session._debate = debate;
|
|
474
|
+
persistDebateState(session);
|
|
475
|
+
|
|
476
|
+
var moderatorProfile = ctx.getMateProfile(mateCtx, mateId);
|
|
477
|
+
ctx.sendToSession(session.localId, {
|
|
478
|
+
type: "debate_brief_ready",
|
|
479
|
+
debateId: debateId,
|
|
480
|
+
topic: debate.topic,
|
|
481
|
+
format: debate.format,
|
|
482
|
+
context: debate.context,
|
|
483
|
+
specialRequests: debate.specialRequests,
|
|
484
|
+
moderatorId: mateId,
|
|
485
|
+
moderatorName: moderatorProfile.name,
|
|
486
|
+
panelists: debate.panelists.map(function (p) {
|
|
487
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
488
|
+
return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
|
|
489
|
+
}),
|
|
490
|
+
});
|
|
491
|
+
return; // Only process first brief found
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- Main debate handlers ---
|
|
496
|
+
|
|
497
|
+
function handleDebateStart(ws, msg) {
|
|
498
|
+
var session = ctx.getSessionForWs(ws);
|
|
499
|
+
if (!session) return;
|
|
500
|
+
|
|
501
|
+
if (!msg.moderatorId || !msg.topic || !msg.panelists || !msg.panelists.length) {
|
|
502
|
+
ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic, panelists." });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (session._debate && (session._debate.phase === "live" || session._debate.phase === "preparing")) {
|
|
507
|
+
ctx.sendTo(ws, { type: "debate_error", error: "A debate is already in progress." });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Block mentions during debate
|
|
512
|
+
if (session._mentionInProgress) {
|
|
513
|
+
ctx.sendTo(ws, { type: "debate_error", error: "A mention is in progress. Wait for it to finish." });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
518
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
519
|
+
var moderatorProfile = ctx.getMateProfile(mateCtx, msg.moderatorId);
|
|
520
|
+
|
|
521
|
+
// --- Phase 1: Preparing (clay-debate-setup skill) ---
|
|
522
|
+
var debate = {
|
|
523
|
+
phase: "preparing",
|
|
524
|
+
topic: msg.topic,
|
|
525
|
+
format: "free_discussion",
|
|
526
|
+
context: "",
|
|
527
|
+
specialRequests: null,
|
|
528
|
+
moderatorId: msg.moderatorId,
|
|
529
|
+
panelists: msg.panelists,
|
|
530
|
+
mateCtx: mateCtx,
|
|
531
|
+
moderatorSession: null,
|
|
532
|
+
panelistSessions: {},
|
|
533
|
+
nameMap: buildDebateNameMap(msg.panelists, mateCtx),
|
|
534
|
+
turnInProgress: false,
|
|
535
|
+
pendingComment: null,
|
|
536
|
+
round: 1,
|
|
537
|
+
history: [],
|
|
538
|
+
setupSessionId: null,
|
|
539
|
+
};
|
|
540
|
+
session._debate = debate;
|
|
541
|
+
|
|
542
|
+
var debateId = "debate_" + Date.now();
|
|
543
|
+
var debateDir = path.join(ctx.cwd, ".clay", "debates", debateId);
|
|
544
|
+
try { fs.mkdirSync(debateDir, { recursive: true }); } catch (e) {}
|
|
545
|
+
var briefPath = path.join(debateDir, "brief.json");
|
|
546
|
+
console.log("[debate] cwd=" + ctx.cwd + " debateDir=" + debateDir + " briefPath=" + briefPath);
|
|
547
|
+
|
|
548
|
+
debate.debateId = debateId;
|
|
549
|
+
debate.briefPath = briefPath;
|
|
550
|
+
|
|
551
|
+
if (msg.quickStart) {
|
|
552
|
+
// --- Quick Start: moderator mate generates brief from DM context ---
|
|
553
|
+
handleDebateQuickStart(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath);
|
|
554
|
+
} else {
|
|
555
|
+
// --- Standard: clay-debate-setup skill ---
|
|
556
|
+
handleDebateSkillSetup(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Quick start: moderator mate uses DM conversation context to generate the debate brief directly
|
|
561
|
+
function handleDebateQuickStart(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath) {
|
|
562
|
+
var debateId = debate.debateId;
|
|
563
|
+
|
|
564
|
+
// Create setup session (still needed for session grouping)
|
|
565
|
+
var setupSession = ctx.sm.createSession();
|
|
566
|
+
setupSession.title = "Debate Setup: " + (msg.topic || "Quick").slice(0, 40);
|
|
567
|
+
setupSession.debateSetupMode = true;
|
|
568
|
+
setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: (msg.topic || "Quick").slice(0, 40), source: "debate", startedAt: Date.now() };
|
|
569
|
+
ctx.sm.saveSessionFile(setupSession);
|
|
570
|
+
ctx.sm.switchSession(setupSession.localId, null, ctx.hydrateImageRefs);
|
|
571
|
+
debate.setupSessionId = setupSession.localId;
|
|
572
|
+
debate.setupStartedAt = setupSession.loop.startedAt;
|
|
573
|
+
|
|
574
|
+
// Build DM conversation context for the moderator
|
|
575
|
+
var dmContext = msg.dmContext || "";
|
|
576
|
+
|
|
577
|
+
// Build panelist info
|
|
578
|
+
var panelistInfo = msg.panelists.map(function (p) {
|
|
579
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
580
|
+
return "- " + (prof.name || p.mateId) + " (ID: " + p.mateId + ", bio: " + (prof.bio || "none") + ")";
|
|
581
|
+
}).join("\n");
|
|
582
|
+
|
|
583
|
+
var quickBriefPrompt = [
|
|
584
|
+
"You are " + (moderatorProfile.name || "the moderator") + ". You were just having a DM conversation with the user, and they want to turn this into a structured debate.",
|
|
585
|
+
"",
|
|
586
|
+
"## Recent DM Conversation",
|
|
587
|
+
dmContext,
|
|
588
|
+
"",
|
|
589
|
+
"## Topic Suggestion",
|
|
590
|
+
msg.topic || "(Derive from conversation above)",
|
|
591
|
+
"",
|
|
592
|
+
"## Available Panelists",
|
|
593
|
+
panelistInfo,
|
|
594
|
+
"",
|
|
595
|
+
"## Your Task",
|
|
596
|
+
"Based on the conversation context, create a debate brief. You know the topic well because you were just discussing it.",
|
|
597
|
+
"Assign each panelist a role and perspective that will create the most productive debate.",
|
|
598
|
+
"",
|
|
599
|
+
"Output ONLY a valid JSON object (no markdown fences, no extra text):",
|
|
600
|
+
"{",
|
|
601
|
+
' "topic": "refined debate topic",',
|
|
602
|
+
' "format": "free_discussion",',
|
|
603
|
+
' "context": "key context from DM conversation that panelists should know",',
|
|
604
|
+
' "specialRequests": "any special instructions (null if none)",',
|
|
605
|
+
' "panelists": [',
|
|
606
|
+
' { "mateId": "...", "role": "perspective/stance", "brief": "what this panelist should argue for" }',
|
|
607
|
+
" ]",
|
|
608
|
+
"}",
|
|
609
|
+
].join("\n");
|
|
610
|
+
|
|
611
|
+
// Persist and start watcher
|
|
612
|
+
persistDebateState(session);
|
|
613
|
+
startDebateBriefWatcher(session, debate, briefPath);
|
|
614
|
+
|
|
615
|
+
// Notify clients
|
|
616
|
+
var preparingMsg = {
|
|
617
|
+
type: "debate_preparing",
|
|
618
|
+
topic: debate.topic || "(Setting up...)",
|
|
619
|
+
moderatorId: debate.moderatorId,
|
|
620
|
+
moderatorName: moderatorProfile.name,
|
|
621
|
+
setupSessionId: setupSession.localId,
|
|
622
|
+
panelists: debate.panelists.map(function (p) {
|
|
623
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
624
|
+
return { mateId: p.mateId, name: prof.name };
|
|
625
|
+
}),
|
|
626
|
+
};
|
|
627
|
+
ctx.sendTo(ws, preparingMsg);
|
|
628
|
+
ctx.sendToSession(session.localId, preparingMsg);
|
|
629
|
+
ctx.sendToSession(setupSession.localId, preparingMsg);
|
|
630
|
+
|
|
631
|
+
// Use moderator's own Claude identity to generate the brief via mention session
|
|
632
|
+
var claudeMd = ctx.loadMateClaudeMd(mateCtx, debate.moderatorId);
|
|
633
|
+
var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
|
|
634
|
+
|
|
635
|
+
var briefText = "";
|
|
636
|
+
ctx.sdk.createMentionSession({
|
|
637
|
+
claudeMd: claudeMd,
|
|
638
|
+
initialContext: digests,
|
|
639
|
+
initialMessage: quickBriefPrompt,
|
|
640
|
+
onActivity: function () {},
|
|
641
|
+
onDelta: function (delta) { briefText += delta; },
|
|
642
|
+
onDone: function () {
|
|
643
|
+
try {
|
|
644
|
+
var cleaned = briefText.trim();
|
|
645
|
+
if (cleaned.indexOf("```") === 0) {
|
|
646
|
+
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
647
|
+
}
|
|
648
|
+
// Validate it is parseable JSON
|
|
649
|
+
JSON.parse(cleaned);
|
|
650
|
+
// Write brief.json for the watcher to pick up
|
|
651
|
+
fs.writeFileSync(briefPath, cleaned, "utf8");
|
|
652
|
+
console.log("[debate-quick] Moderator generated brief, wrote to " + briefPath);
|
|
653
|
+
} catch (e) {
|
|
654
|
+
console.error("[debate-quick] Failed to generate brief:", e.message);
|
|
655
|
+
console.error("[debate-quick] Raw output:", briefText.substring(0, 500));
|
|
656
|
+
// Fall back: write a minimal brief
|
|
657
|
+
var fallbackBrief = {
|
|
658
|
+
topic: debate.topic || "Discussion",
|
|
659
|
+
format: "free_discussion",
|
|
660
|
+
context: "",
|
|
661
|
+
specialRequests: null,
|
|
662
|
+
panelists: debate.panelists.map(function (p) {
|
|
663
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
664
|
+
return { mateId: p.mateId, role: "participant", brief: "Share your perspective on the topic." };
|
|
665
|
+
}),
|
|
666
|
+
};
|
|
667
|
+
try {
|
|
668
|
+
fs.writeFileSync(briefPath, JSON.stringify(fallbackBrief), "utf8");
|
|
669
|
+
console.log("[debate-quick] Wrote fallback brief");
|
|
670
|
+
} catch (fe) {
|
|
671
|
+
console.error("[debate-quick] Failed to write fallback brief:", fe.message);
|
|
672
|
+
endDebate(session, "error");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
onError: function (err) {
|
|
677
|
+
console.error("[debate-quick] Moderator brief generation failed:", err);
|
|
678
|
+
endDebate(session, "error");
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Standard debate setup via clay-debate-setup skill
|
|
684
|
+
function handleDebateSkillSetup(ws, session, debate, msg, mateCtx, moderatorProfile, briefPath) {
|
|
685
|
+
var debateId = debate.debateId;
|
|
686
|
+
|
|
687
|
+
// Create a new session for the setup skill (like Ralph crafting)
|
|
688
|
+
var setupSession = ctx.sm.createSession();
|
|
689
|
+
setupSession.title = "Debate Setup: " + msg.topic.slice(0, 40);
|
|
690
|
+
setupSession.debateSetupMode = true;
|
|
691
|
+
setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: msg.topic.slice(0, 40), source: "debate", startedAt: Date.now() };
|
|
692
|
+
ctx.sm.saveSessionFile(setupSession);
|
|
693
|
+
ctx.sm.switchSession(setupSession.localId, null, ctx.hydrateImageRefs);
|
|
694
|
+
debate.setupSessionId = setupSession.localId;
|
|
695
|
+
debate.setupStartedAt = setupSession.loop.startedAt;
|
|
696
|
+
|
|
697
|
+
// Build panelist info for the skill prompt
|
|
698
|
+
var panelistNames = msg.panelists.map(function (p) {
|
|
699
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
700
|
+
return prof.name || p.mateId;
|
|
701
|
+
}).join(", ");
|
|
702
|
+
|
|
703
|
+
var craftingPrompt = "Use the /clay-debate-setup skill to prepare a structured debate. " +
|
|
704
|
+
"You MUST invoke the clay-debate-setup skill. Do NOT start the debate yourself.\n\n" +
|
|
705
|
+
"## Initial Topic\n" + msg.topic + "\n\n" +
|
|
706
|
+
"## Moderator\n" + (moderatorProfile.name || msg.moderatorId) + "\n\n" +
|
|
707
|
+
"## Selected Panelists\n" + msg.panelists.map(function (p) {
|
|
708
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
709
|
+
return "- " + (prof.name || p.mateId) + " (ID: " + p.mateId + ")";
|
|
710
|
+
}).join("\n") + "\n\n" +
|
|
711
|
+
"## Debate Brief Output Path\n" +
|
|
712
|
+
"When the setup is complete, write the debate brief JSON to this EXACT absolute path:\n" +
|
|
713
|
+
"`" + briefPath + "`\n" +
|
|
714
|
+
"This is where the debate engine watches for the file. Do NOT write it anywhere else.\n\n" +
|
|
715
|
+
"## Spoken Language\nKorean (unless user switches)";
|
|
716
|
+
|
|
717
|
+
// Persist debate state before starting watcher
|
|
718
|
+
persistDebateState(session);
|
|
719
|
+
|
|
720
|
+
// Watch for brief.json in the debate-specific directory
|
|
721
|
+
startDebateBriefWatcher(session, debate, briefPath);
|
|
722
|
+
|
|
723
|
+
// Notify clients that we are in preparing phase (send to both original and setup session)
|
|
724
|
+
var preparingMsg = {
|
|
725
|
+
type: "debate_preparing",
|
|
726
|
+
topic: debate.topic,
|
|
727
|
+
moderatorId: debate.moderatorId,
|
|
728
|
+
moderatorName: moderatorProfile.name,
|
|
729
|
+
setupSessionId: setupSession.localId,
|
|
730
|
+
panelists: debate.panelists.map(function (p) {
|
|
731
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
732
|
+
return { mateId: p.mateId, name: prof.name };
|
|
733
|
+
}),
|
|
734
|
+
};
|
|
735
|
+
// Send directly to the requesting ws (session switch may not have propagated yet)
|
|
736
|
+
ctx.sendTo(ws, preparingMsg);
|
|
737
|
+
// Also broadcast to any other clients on either session
|
|
738
|
+
ctx.sendToSession(session.localId, preparingMsg);
|
|
739
|
+
ctx.sendToSession(setupSession.localId, preparingMsg);
|
|
740
|
+
|
|
741
|
+
// Start the setup skill session
|
|
742
|
+
setupSession.history.push({ type: "user_message", text: craftingPrompt });
|
|
743
|
+
ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt });
|
|
744
|
+
ctx.sendToSession(setupSession.localId, { type: "user_message", text: craftingPrompt });
|
|
745
|
+
setupSession.isProcessing = true;
|
|
746
|
+
ctx.onProcessingChanged();
|
|
747
|
+
setupSession.sentToolResults = {};
|
|
748
|
+
ctx.sendToSession(setupSession.localId, { type: "status", status: "processing" });
|
|
749
|
+
ctx.sdk.startQuery(setupSession, craftingPrompt, undefined, ctx.getLinuxUserForSession(setupSession));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// --- Live debate ---
|
|
753
|
+
|
|
754
|
+
function startDebateLive(session) {
|
|
755
|
+
var debate = session._debate;
|
|
756
|
+
if (!debate || debate.phase === "live") return;
|
|
757
|
+
|
|
758
|
+
debate.phase = "live";
|
|
759
|
+
debate.turnInProgress = true;
|
|
760
|
+
debate.round = 1;
|
|
761
|
+
|
|
762
|
+
var mateCtx = debate.mateCtx;
|
|
763
|
+
var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
|
|
764
|
+
|
|
765
|
+
// Create a dedicated debate session, grouped with the setup session
|
|
766
|
+
var debateSession = ctx.sm.createSession();
|
|
767
|
+
debateSession.title = debate.topic.slice(0, 50);
|
|
768
|
+
debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
|
|
769
|
+
// Assign cliSessionId manually so saveSessionFile works (no SDK query for debate sessions)
|
|
770
|
+
if (!debateSession.cliSessionId) {
|
|
771
|
+
debateSession.cliSessionId = crypto.randomUUID();
|
|
772
|
+
}
|
|
773
|
+
ctx.sm.saveSessionFile(debateSession);
|
|
774
|
+
ctx.sm.switchSession(debateSession.localId, null, ctx.hydrateImageRefs);
|
|
775
|
+
debate.liveSessionId = debateSession.localId;
|
|
776
|
+
|
|
777
|
+
// Move _debate to the new session so all debate logic uses it
|
|
778
|
+
debateSession._debate = debate;
|
|
779
|
+
delete session._debate;
|
|
780
|
+
// Clear persisted state from setup session, persist on live session
|
|
781
|
+
session.debateState = null;
|
|
782
|
+
ctx.sm.saveSessionFile(session);
|
|
783
|
+
persistDebateState(debateSession);
|
|
784
|
+
|
|
785
|
+
// Save to session history
|
|
786
|
+
var debateStartEntry = {
|
|
787
|
+
type: "debate_started",
|
|
788
|
+
topic: debate.topic,
|
|
789
|
+
format: debate.format,
|
|
790
|
+
moderatorId: debate.moderatorId,
|
|
791
|
+
moderatorName: moderatorProfile.name,
|
|
792
|
+
panelists: debate.panelists.map(function (p) {
|
|
793
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
794
|
+
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
795
|
+
}),
|
|
796
|
+
};
|
|
797
|
+
debateSession.history.push(debateStartEntry);
|
|
798
|
+
ctx.sm.appendToSessionFile(debateSession, debateStartEntry);
|
|
799
|
+
|
|
800
|
+
// Notify clients (same data as history entry)
|
|
801
|
+
ctx.sendToSession(debateSession.localId, debateStartEntry);
|
|
802
|
+
|
|
803
|
+
// Signal moderator's first turn
|
|
804
|
+
ctx.sendToSession(debateSession.localId, {
|
|
805
|
+
type: "debate_turn",
|
|
806
|
+
mateId: debate.moderatorId,
|
|
807
|
+
mateName: moderatorProfile.name,
|
|
808
|
+
role: "moderator",
|
|
809
|
+
round: debate.round,
|
|
810
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
811
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
812
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Create moderator mention session
|
|
816
|
+
var claudeMd = ctx.loadMateClaudeMd(mateCtx, debate.moderatorId);
|
|
817
|
+
var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
|
|
818
|
+
var moderatorContext = buildModeratorContext(debate) + digests;
|
|
819
|
+
|
|
820
|
+
ctx.sdk.createMentionSession({
|
|
821
|
+
claudeMd: claudeMd,
|
|
822
|
+
initialContext: moderatorContext,
|
|
823
|
+
initialMessage: "Begin the debate on: " + debate.topic,
|
|
824
|
+
onActivity: function (activity) {
|
|
825
|
+
if (debateSession._debate && debateSession._debate.phase !== "ended") {
|
|
826
|
+
ctx.sendToSession(debateSession.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
onDelta: function (delta) {
|
|
830
|
+
if (debateSession._debate && debateSession._debate.phase !== "ended") {
|
|
831
|
+
ctx.sendToSession(debateSession.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
onDone: function (fullText) {
|
|
835
|
+
handleModeratorTurnDone(debateSession, fullText);
|
|
836
|
+
},
|
|
837
|
+
onError: function (errMsg) {
|
|
838
|
+
console.error("[debate] Moderator error:", errMsg);
|
|
839
|
+
endDebate(debateSession, "error");
|
|
840
|
+
},
|
|
841
|
+
canUseTool: buildDebateToolHandler(debateSession),
|
|
842
|
+
}).then(function (mentionSession) {
|
|
843
|
+
if (mentionSession) {
|
|
844
|
+
debate.moderatorSession = mentionSession;
|
|
845
|
+
}
|
|
846
|
+
}).catch(function (err) {
|
|
847
|
+
console.error("[debate] Failed to create moderator session:", err.message || err);
|
|
848
|
+
endDebate(debateSession, "error");
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// --- Turn management ---
|
|
853
|
+
|
|
854
|
+
function handleModeratorTurnDone(session, fullText) {
|
|
855
|
+
var debate = session._debate;
|
|
856
|
+
if (!debate || debate.phase === "ended") return;
|
|
857
|
+
|
|
858
|
+
debate.turnInProgress = false;
|
|
859
|
+
|
|
860
|
+
// Record in debate history
|
|
861
|
+
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
862
|
+
debate.history.push({ speaker: "moderator", mateId: debate.moderatorId, mateName: moderatorProfile.name, text: fullText });
|
|
863
|
+
|
|
864
|
+
// Save to session history
|
|
865
|
+
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 };
|
|
866
|
+
session.history.push(turnEntry);
|
|
867
|
+
ctx.sm.appendToSessionFile(session, turnEntry);
|
|
868
|
+
ctx.sendToSession(session.localId, turnEntry);
|
|
869
|
+
|
|
870
|
+
// Check if user stopped the debate during this turn
|
|
871
|
+
if (debate.phase === "ending") {
|
|
872
|
+
endDebate(session, "user_stopped");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Detect @mentions
|
|
877
|
+
console.log("[debate] nameMap keys:", JSON.stringify(Object.keys(debate.nameMap)));
|
|
878
|
+
console.log("[debate] moderator text (last 200):", fullText.slice(-200));
|
|
879
|
+
var mentionedIds = detectMentions(fullText, debate.nameMap);
|
|
880
|
+
console.log("[debate] detected mentions:", JSON.stringify(mentionedIds));
|
|
881
|
+
|
|
882
|
+
if (mentionedIds.length === 0) {
|
|
883
|
+
// No mentions = moderator wants to conclude. Ask user to confirm.
|
|
884
|
+
console.log("[debate] No mentions detected, requesting user confirmation to end.");
|
|
885
|
+
debate.turnInProgress = false;
|
|
886
|
+
debate.awaitingConcludeConfirm = true;
|
|
887
|
+
persistDebateState(session);
|
|
888
|
+
var concludeEntry = { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round };
|
|
889
|
+
session.history.push(concludeEntry);
|
|
890
|
+
ctx.sm.appendToSessionFile(session, concludeEntry);
|
|
891
|
+
ctx.sendToSession(session.localId, concludeEntry);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Check for pending user comment before triggering panelist
|
|
896
|
+
if (debate.pendingComment) {
|
|
897
|
+
injectUserComment(session);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Trigger the first mentioned panelist
|
|
902
|
+
triggerPanelist(session, mentionedIds[0], fullText);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function triggerPanelist(session, mateId, moderatorText) {
|
|
906
|
+
var debate = session._debate;
|
|
907
|
+
if (!debate || debate.phase === "ended") return;
|
|
908
|
+
|
|
909
|
+
debate.turnInProgress = true;
|
|
910
|
+
debate._currentTurnMateId = mateId;
|
|
911
|
+
debate._currentTurnText = "";
|
|
912
|
+
|
|
913
|
+
var profile = ctx.getMateProfile(debate.mateCtx, mateId);
|
|
914
|
+
var panelistInfo = null;
|
|
915
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
916
|
+
if (debate.panelists[i].mateId === mateId) {
|
|
917
|
+
panelistInfo = debate.panelists[i];
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (!panelistInfo) {
|
|
922
|
+
console.error("[debate] Panelist not found:", mateId);
|
|
923
|
+
debate._currentTurnMateId = null;
|
|
924
|
+
// Feed error back to moderator
|
|
925
|
+
feedBackToModerator(session, mateId, "[This panelist is not part of the debate panel.]");
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Notify clients of new turn
|
|
930
|
+
ctx.sendToSession(session.localId, {
|
|
931
|
+
type: "debate_turn",
|
|
932
|
+
mateId: mateId,
|
|
933
|
+
mateName: profile.name,
|
|
934
|
+
role: panelistInfo.role,
|
|
935
|
+
round: debate.round,
|
|
936
|
+
avatarColor: profile.avatarColor,
|
|
937
|
+
avatarStyle: profile.avatarStyle,
|
|
938
|
+
avatarSeed: profile.avatarSeed,
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
var panelistCallbacks = {
|
|
942
|
+
onActivity: function (activity) {
|
|
943
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
944
|
+
ctx.sendToSession(session.localId, { type: "debate_activity", mateId: mateId, activity: activity });
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
onDelta: function (delta) {
|
|
948
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
949
|
+
debate._currentTurnText += delta;
|
|
950
|
+
ctx.sendToSession(session.localId, { type: "debate_stream", mateId: mateId, mateName: profile.name, delta: delta });
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
onDone: function (fullText) {
|
|
954
|
+
handlePanelistTurnDone(session, mateId, fullText);
|
|
955
|
+
},
|
|
956
|
+
onError: function (errMsg) {
|
|
957
|
+
console.error("[debate] Panelist error for " + mateId + ":", errMsg);
|
|
958
|
+
debate.turnInProgress = false;
|
|
959
|
+
// Feed error back to moderator so the debate can continue
|
|
960
|
+
feedBackToModerator(session, mateId, "[" + profile.name + " encountered an error and could not respond. Please continue with other panelists or wrap up.]");
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// Check for existing session
|
|
965
|
+
var existing = debate.panelistSessions[mateId];
|
|
966
|
+
if (existing && existing.isAlive()) {
|
|
967
|
+
// Build recent debate context for continuation
|
|
968
|
+
var recentHistory = "";
|
|
969
|
+
var lastPanelistIdx = -1;
|
|
970
|
+
for (var hi = debate.history.length - 1; hi >= 0; hi--) {
|
|
971
|
+
if (debate.history[hi].mateId === mateId) {
|
|
972
|
+
lastPanelistIdx = hi;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (lastPanelistIdx >= 0 && lastPanelistIdx < debate.history.length - 1) {
|
|
977
|
+
recentHistory = "\n\n[Debate turns since your last response:]\n---\n";
|
|
978
|
+
for (var hj = lastPanelistIdx + 1; hj < debate.history.length; hj++) {
|
|
979
|
+
var h = debate.history[hj];
|
|
980
|
+
recentHistory += h.mateName + " (" + (h.speaker === "moderator" ? "moderator" : h.role || h.speaker) + "): " + h.text.substring(0, 500) + "\n\n";
|
|
981
|
+
}
|
|
982
|
+
recentHistory += "---";
|
|
983
|
+
}
|
|
984
|
+
var continuationMsg = recentHistory + "\n\n[The moderator is now addressing you. Please respond.]\n\nModerator said:\n" + moderatorText;
|
|
985
|
+
existing.pushMessage(continuationMsg, panelistCallbacks);
|
|
986
|
+
} else {
|
|
987
|
+
// Create new panelist session
|
|
988
|
+
var claudeMd = ctx.loadMateClaudeMd(debate.mateCtx, mateId);
|
|
989
|
+
var digests = ctx.loadMateDigests(debate.mateCtx, mateId, debate.topic);
|
|
990
|
+
var panelistContext = buildPanelistContext(debate, panelistInfo) + digests;
|
|
991
|
+
|
|
992
|
+
// Include debate history so far for context
|
|
993
|
+
var historyContext = "";
|
|
994
|
+
if (debate.history.length > 0) {
|
|
995
|
+
historyContext = "\n\n[Debate so far:]\n---\n";
|
|
996
|
+
for (var hk = 0; hk < debate.history.length; hk++) {
|
|
997
|
+
var he = debate.history[hk];
|
|
998
|
+
historyContext += he.mateName + " (" + (he.speaker === "moderator" ? "moderator" : he.role || he.speaker) + "): " + he.text.substring(0, 500) + "\n\n";
|
|
999
|
+
}
|
|
1000
|
+
historyContext += "---";
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
ctx.sdk.createMentionSession({
|
|
1004
|
+
claudeMd: claudeMd,
|
|
1005
|
+
initialContext: panelistContext + historyContext,
|
|
1006
|
+
initialMessage: "The moderator addresses you:\n\n" + moderatorText,
|
|
1007
|
+
onActivity: panelistCallbacks.onActivity,
|
|
1008
|
+
onDelta: panelistCallbacks.onDelta,
|
|
1009
|
+
onDone: panelistCallbacks.onDone,
|
|
1010
|
+
onError: panelistCallbacks.onError,
|
|
1011
|
+
canUseTool: buildDebateToolHandler(session),
|
|
1012
|
+
}).then(function (mentionSession) {
|
|
1013
|
+
if (mentionSession) {
|
|
1014
|
+
debate.panelistSessions[mateId] = mentionSession;
|
|
1015
|
+
}
|
|
1016
|
+
}).catch(function (err) {
|
|
1017
|
+
console.error("[debate] Failed to create panelist session for " + mateId + ":", err.message || err);
|
|
1018
|
+
debate.turnInProgress = false;
|
|
1019
|
+
feedBackToModerator(session, mateId, "[" + profile.name + " is unavailable. Please continue with other panelists or wrap up.]");
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function handlePanelistTurnDone(session, mateId, fullText) {
|
|
1025
|
+
var debate = session._debate;
|
|
1026
|
+
if (!debate || debate.phase === "ended") return;
|
|
1027
|
+
|
|
1028
|
+
debate.turnInProgress = false;
|
|
1029
|
+
debate._currentTurnMateId = null;
|
|
1030
|
+
debate._currentTurnText = "";
|
|
1031
|
+
|
|
1032
|
+
var profile = ctx.getMateProfile(debate.mateCtx, mateId);
|
|
1033
|
+
var panelistInfo = null;
|
|
1034
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
1035
|
+
if (debate.panelists[i].mateId === mateId) {
|
|
1036
|
+
panelistInfo = debate.panelists[i];
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Record in debate history
|
|
1042
|
+
debate.history.push({ speaker: "panelist", mateId: mateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: fullText });
|
|
1043
|
+
|
|
1044
|
+
// Save to session history
|
|
1045
|
+
var turnEntry = { type: "debate_turn_done", mateId: mateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", round: debate.round, text: fullText, avatarStyle: profile.avatarStyle, avatarSeed: profile.avatarSeed, avatarColor: profile.avatarColor };
|
|
1046
|
+
session.history.push(turnEntry);
|
|
1047
|
+
ctx.sm.appendToSessionFile(session, turnEntry);
|
|
1048
|
+
ctx.sendToSession(session.localId, turnEntry);
|
|
1049
|
+
|
|
1050
|
+
// Check if user stopped the debate
|
|
1051
|
+
if (debate.phase === "ending") {
|
|
1052
|
+
endDebate(session, "user_stopped");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Check for pending user comment
|
|
1057
|
+
if (debate.pendingComment) {
|
|
1058
|
+
injectUserComment(session);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Feed panelist response back to moderator
|
|
1063
|
+
feedBackToModerator(session, mateId, fullText);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function feedBackToModerator(session, panelistMateId, panelistText) {
|
|
1067
|
+
var debate = session._debate;
|
|
1068
|
+
if (!debate || !debate.moderatorSession || debate.phase === "ended") return;
|
|
1069
|
+
|
|
1070
|
+
debate.round++;
|
|
1071
|
+
debate.turnInProgress = true;
|
|
1072
|
+
|
|
1073
|
+
var panelistProfile = ctx.getMateProfile(debate.mateCtx, panelistMateId);
|
|
1074
|
+
var panelistInfo = null;
|
|
1075
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
1076
|
+
if (debate.panelists[i].mateId === panelistMateId) {
|
|
1077
|
+
panelistInfo = debate.panelists[i];
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1083
|
+
|
|
1084
|
+
// Notify clients of moderator turn
|
|
1085
|
+
ctx.sendToSession(session.localId, {
|
|
1086
|
+
type: "debate_turn",
|
|
1087
|
+
mateId: debate.moderatorId,
|
|
1088
|
+
mateName: moderatorProfile.name,
|
|
1089
|
+
role: "moderator",
|
|
1090
|
+
round: debate.round,
|
|
1091
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
1092
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
1093
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
var feedText = "[Panelist Response]\n\n" +
|
|
1097
|
+
"@" + panelistProfile.name + " (" + (panelistInfo ? panelistInfo.role : "panelist") + ") responded:\n" +
|
|
1098
|
+
panelistText + "\n\n" +
|
|
1099
|
+
"Continue the debate. Call on the next panelist with @TheirName, or provide a closing summary (without any @mentions) to end the debate.";
|
|
1100
|
+
|
|
1101
|
+
debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function buildModeratorCallbacks(session) {
|
|
1105
|
+
var debate = session._debate;
|
|
1106
|
+
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1107
|
+
return {
|
|
1108
|
+
onActivity: function (activity) {
|
|
1109
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
1110
|
+
ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
onDelta: function (delta) {
|
|
1114
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
1115
|
+
ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
onDone: function (fullText) {
|
|
1119
|
+
handleModeratorTurnDone(session, fullText);
|
|
1120
|
+
},
|
|
1121
|
+
onError: function (errMsg) {
|
|
1122
|
+
console.error("[debate] Moderator error:", errMsg);
|
|
1123
|
+
endDebate(session, "error");
|
|
1124
|
+
},
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// --- User interaction during debate ---
|
|
1129
|
+
|
|
1130
|
+
function handleDebateComment(ws, msg) {
|
|
1131
|
+
var session = ctx.getSessionForWs(ws);
|
|
1132
|
+
if (!session) return;
|
|
1133
|
+
|
|
1134
|
+
var debate = session._debate;
|
|
1135
|
+
if (!debate || debate.phase !== "live") {
|
|
1136
|
+
ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// If awaiting conclude confirmation, re-send the confirm prompt instead
|
|
1141
|
+
if (debate.awaitingConcludeConfirm) {
|
|
1142
|
+
ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (!msg.text) return;
|
|
1147
|
+
|
|
1148
|
+
debate.pendingComment = { text: msg.text };
|
|
1149
|
+
ctx.sendToSession(session.localId, { type: "debate_comment_queued", text: msg.text });
|
|
1150
|
+
|
|
1151
|
+
// If a panelist turn is in progress, abort it and go straight to moderator
|
|
1152
|
+
if (debate.turnInProgress && debate._currentTurnMateId && debate._currentTurnMateId !== debate.moderatorId) {
|
|
1153
|
+
var abortMateId = debate._currentTurnMateId;
|
|
1154
|
+
console.log("[debate] User raised hand during panelist turn, aborting " + abortMateId);
|
|
1155
|
+
|
|
1156
|
+
// Close the panelist's mention session to stop generation
|
|
1157
|
+
if (debate.panelistSessions[abortMateId]) {
|
|
1158
|
+
try { debate.panelistSessions[abortMateId].close(); } catch (e) {}
|
|
1159
|
+
delete debate.panelistSessions[abortMateId];
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Save partial text as interrupted turn
|
|
1163
|
+
var partialText = debate._currentTurnText || "(interrupted by audience)";
|
|
1164
|
+
var profile = ctx.getMateProfile(debate.mateCtx, abortMateId);
|
|
1165
|
+
var panelistInfo = null;
|
|
1166
|
+
for (var pi = 0; pi < debate.panelists.length; pi++) {
|
|
1167
|
+
if (debate.panelists[pi].mateId === abortMateId) { panelistInfo = debate.panelists[pi]; break; }
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
ctx.sendToSession(session.localId, {
|
|
1171
|
+
type: "debate_turn_done",
|
|
1172
|
+
mateId: abortMateId,
|
|
1173
|
+
mateName: profile.name,
|
|
1174
|
+
role: panelistInfo ? panelistInfo.role : "",
|
|
1175
|
+
text: partialText,
|
|
1176
|
+
interrupted: true,
|
|
1177
|
+
avatarStyle: profile.avatarStyle,
|
|
1178
|
+
avatarSeed: profile.avatarSeed,
|
|
1179
|
+
avatarColor: profile.avatarColor,
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
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 };
|
|
1183
|
+
session.history.push(turnEntry);
|
|
1184
|
+
ctx.sm.appendToSessionFile(session, turnEntry);
|
|
1185
|
+
debate.history.push({ speaker: "panelist", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: partialText });
|
|
1186
|
+
|
|
1187
|
+
debate.turnInProgress = false;
|
|
1188
|
+
debate._currentTurnMateId = null;
|
|
1189
|
+
debate._currentTurnText = "";
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Inject to moderator immediately if no turn in progress (or just aborted)
|
|
1193
|
+
if (!debate.turnInProgress) {
|
|
1194
|
+
injectUserComment(session);
|
|
1195
|
+
}
|
|
1196
|
+
// If moderator is currently speaking, pendingComment will be picked up after moderator's onDone
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function injectUserComment(session) {
|
|
1200
|
+
var debate = session._debate;
|
|
1201
|
+
if (!debate || !debate.pendingComment || !debate.moderatorSession || debate.phase === "ended") return;
|
|
1202
|
+
|
|
1203
|
+
var comment = debate.pendingComment;
|
|
1204
|
+
debate.pendingComment = null;
|
|
1205
|
+
|
|
1206
|
+
// Record in debate history
|
|
1207
|
+
debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: comment.text });
|
|
1208
|
+
|
|
1209
|
+
var commentEntry = { type: "debate_comment_injected", text: comment.text };
|
|
1210
|
+
session.history.push(commentEntry);
|
|
1211
|
+
ctx.sm.appendToSessionFile(session, commentEntry);
|
|
1212
|
+
ctx.sendToSession(session.localId, commentEntry);
|
|
1213
|
+
|
|
1214
|
+
// Feed to moderator
|
|
1215
|
+
debate.turnInProgress = true;
|
|
1216
|
+
var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
1217
|
+
|
|
1218
|
+
ctx.sendToSession(session.localId, {
|
|
1219
|
+
type: "debate_turn",
|
|
1220
|
+
mateId: debate.moderatorId,
|
|
1221
|
+
mateName: moderatorProfile.name,
|
|
1222
|
+
role: "moderator",
|
|
1223
|
+
round: debate.round,
|
|
1224
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
1225
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
1226
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
var feedText = "[The user raised their hand and said:]\n" +
|
|
1230
|
+
comment.text + "\n" +
|
|
1231
|
+
"[Please acknowledge this and weave it into the discussion. Then continue the debate.]";
|
|
1232
|
+
|
|
1233
|
+
debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function handleDebateConfirmBrief(ws) {
|
|
1237
|
+
var session = ctx.getSessionForWs(ws);
|
|
1238
|
+
if (!session) return;
|
|
1239
|
+
|
|
1240
|
+
var debate = session._debate;
|
|
1241
|
+
if (!debate || debate.phase !== "reviewing") {
|
|
1242
|
+
ctx.sendTo(ws, { type: "debate_error", error: "No debate brief to confirm." });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
console.log("[debate] User confirmed brief, transitioning to live. Topic:", debate.topic);
|
|
1247
|
+
startDebateLive(session);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function handleDebateStop(ws) {
|
|
1251
|
+
var session = ctx.getSessionForWs(ws);
|
|
1252
|
+
if (!session) return;
|
|
1253
|
+
|
|
1254
|
+
var debate = session._debate;
|
|
1255
|
+
if (!debate) return;
|
|
1256
|
+
|
|
1257
|
+
if (debate.phase === "reviewing") {
|
|
1258
|
+
endDebate(session, "user_stopped");
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (debate.phase !== "live") return;
|
|
1263
|
+
|
|
1264
|
+
if (debate.turnInProgress) {
|
|
1265
|
+
// Let current turn finish, then end
|
|
1266
|
+
debate.phase = "ending";
|
|
1267
|
+
} else {
|
|
1268
|
+
endDebate(session, "user_stopped");
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Rebuild _debate from session history (for resume after server restart)
|
|
1273
|
+
function rebuildDebateState(session, ws) {
|
|
1274
|
+
// Find debate_started entry in history
|
|
1275
|
+
var startEntry = null;
|
|
1276
|
+
var endEntry = null;
|
|
1277
|
+
var concludeEntry = null;
|
|
1278
|
+
var lastRound = 1;
|
|
1279
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
1280
|
+
var h = session.history[i];
|
|
1281
|
+
if (h.type === "debate_started") startEntry = h;
|
|
1282
|
+
if (h.type === "debate_ended") endEntry = h;
|
|
1283
|
+
if (h.type === "debate_conclude_confirm") concludeEntry = h;
|
|
1284
|
+
if (h.type === "debate_turn_done" && h.round) lastRound = h.round;
|
|
1285
|
+
}
|
|
1286
|
+
if (!startEntry) return null;
|
|
1287
|
+
|
|
1288
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1289
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
1290
|
+
|
|
1291
|
+
var debate = {
|
|
1292
|
+
phase: endEntry ? "ended" : "live",
|
|
1293
|
+
topic: startEntry.topic || "",
|
|
1294
|
+
format: startEntry.format || "free_discussion",
|
|
1295
|
+
context: "",
|
|
1296
|
+
specialRequests: null,
|
|
1297
|
+
moderatorId: startEntry.moderatorId,
|
|
1298
|
+
panelists: (startEntry.panelists || []).map(function (p) {
|
|
1299
|
+
return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
|
|
1300
|
+
}),
|
|
1301
|
+
mateCtx: mateCtx,
|
|
1302
|
+
moderatorSession: null,
|
|
1303
|
+
panelistSessions: {},
|
|
1304
|
+
nameMap: buildDebateNameMap(
|
|
1305
|
+
(startEntry.panelists || []).map(function (p) { return { mateId: p.mateId, role: p.role || "" }; }),
|
|
1306
|
+
mateCtx
|
|
1307
|
+
),
|
|
1308
|
+
turnInProgress: false,
|
|
1309
|
+
pendingComment: null,
|
|
1310
|
+
round: lastRound,
|
|
1311
|
+
history: [],
|
|
1312
|
+
awaitingConcludeConfirm: !endEntry && !!concludeEntry,
|
|
1313
|
+
debateId: (session.loop && session.loop.loopId) || "debate_rebuilt",
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
// Rebuild debate.history from session history turn entries
|
|
1317
|
+
for (var j = 0; j < session.history.length; j++) {
|
|
1318
|
+
var entry = session.history[j];
|
|
1319
|
+
if (entry.type === "debate_turn_done") {
|
|
1320
|
+
debate.history.push({
|
|
1321
|
+
speaker: entry.role === "moderator" ? "moderator" : "panelist",
|
|
1322
|
+
mateId: entry.mateId,
|
|
1323
|
+
mateName: entry.mateName,
|
|
1324
|
+
role: entry.role || "",
|
|
1325
|
+
text: entry.text || "",
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// If no endEntry and no concludeEntry, check if last moderator turn had no mentions (implicit conclude)
|
|
1331
|
+
if (!endEntry && !concludeEntry && debate.history.length > 0) {
|
|
1332
|
+
var lastTurn = debate.history[debate.history.length - 1];
|
|
1333
|
+
if (lastTurn.speaker === "moderator" && lastTurn.text) {
|
|
1334
|
+
var rebuildMentions = detectMentions(lastTurn.text, debate.nameMap);
|
|
1335
|
+
if (rebuildMentions.length === 0) {
|
|
1336
|
+
debate.awaitingConcludeConfirm = true;
|
|
1337
|
+
console.log("[debate] Last moderator turn had no mentions, setting awaitingConcludeConfirm.");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
session._debate = debate;
|
|
1343
|
+
console.log("[debate] Rebuilt debate state from history. Topic:", debate.topic, "Phase:", debate.phase, "Turns:", debate.history.length);
|
|
1344
|
+
return debate;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function handleDebateConcludeResponse(ws, msg) {
|
|
1348
|
+
var session = ctx.getSessionForWs(ws);
|
|
1349
|
+
if (!session) return;
|
|
1350
|
+
var debate = session._debate;
|
|
1351
|
+
|
|
1352
|
+
// If _debate is gone (server restart), try to rebuild from history
|
|
1353
|
+
if (!debate) {
|
|
1354
|
+
debate = rebuildDebateState(session, ws);
|
|
1355
|
+
if (!debate) {
|
|
1356
|
+
console.log("[debate] Cannot rebuild debate state for resume.");
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Allow resume from both "live + awaiting confirm" and "ended" states
|
|
1362
|
+
var isLiveConfirm = debate.phase === "live" && debate.awaitingConcludeConfirm;
|
|
1363
|
+
var isResume = debate.phase === "ended" && msg.action === "continue";
|
|
1364
|
+
if (!isLiveConfirm && !isResume) return;
|
|
1365
|
+
|
|
1366
|
+
debate.awaitingConcludeConfirm = false;
|
|
1367
|
+
|
|
1368
|
+
if (msg.action === "end") {
|
|
1369
|
+
endDebate(session, "natural");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (msg.action === "continue") {
|
|
1374
|
+
var wasEnded = debate.phase === "ended";
|
|
1375
|
+
debate.phase = "live";
|
|
1376
|
+
var instruction = (msg.text || "").trim();
|
|
1377
|
+
var mateCtx = debate.mateCtx || matesModule.buildMateCtx(ws._clayUser ? ws._clayUser.id : null);
|
|
1378
|
+
debate.mateCtx = mateCtx;
|
|
1379
|
+
var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
|
|
1380
|
+
|
|
1381
|
+
// Record user's resume message if provided
|
|
1382
|
+
if (instruction) {
|
|
1383
|
+
var resumeEntry = { type: "debate_user_resume", text: instruction };
|
|
1384
|
+
session.history.push(resumeEntry);
|
|
1385
|
+
ctx.sm.appendToSessionFile(session, resumeEntry);
|
|
1386
|
+
ctx.sendToSession(session.localId, resumeEntry);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Notify clients debate is back live and persist to history
|
|
1390
|
+
var resumedMsg = {
|
|
1391
|
+
type: "debate_resumed",
|
|
1392
|
+
topic: debate.topic,
|
|
1393
|
+
round: debate.round,
|
|
1394
|
+
moderatorId: debate.moderatorId,
|
|
1395
|
+
moderatorName: moderatorProfile.name,
|
|
1396
|
+
panelists: debate.panelists.map(function (p) {
|
|
1397
|
+
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
1398
|
+
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
1399
|
+
}),
|
|
1400
|
+
};
|
|
1401
|
+
session.history.push(resumedMsg);
|
|
1402
|
+
ctx.sm.appendToSessionFile(session, resumedMsg);
|
|
1403
|
+
ctx.sendToSession(session.localId, resumedMsg);
|
|
1404
|
+
|
|
1405
|
+
debate.turnInProgress = true;
|
|
1406
|
+
ctx.sendToSession(session.localId, {
|
|
1407
|
+
type: "debate_turn",
|
|
1408
|
+
mateId: debate.moderatorId,
|
|
1409
|
+
mateName: moderatorProfile.name,
|
|
1410
|
+
role: "moderator",
|
|
1411
|
+
round: debate.round,
|
|
1412
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
1413
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
1414
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
var resumePrompt = instruction
|
|
1418
|
+
? "[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.]"
|
|
1419
|
+
: "[The audience has requested the debate continue. Call on the next panelist with @TheirName to explore additional perspectives.]";
|
|
1420
|
+
|
|
1421
|
+
// If resuming from ended state, moderator session may be dead. Create a new one.
|
|
1422
|
+
if (wasEnded || !debate.moderatorSession || !debate.moderatorSession.isAlive()) {
|
|
1423
|
+
console.log("[debate] Creating new moderator session for resume");
|
|
1424
|
+
var claudeMd = ctx.loadMateClaudeMd(mateCtx, debate.moderatorId);
|
|
1425
|
+
var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
|
|
1426
|
+
var moderatorContext = buildModeratorContext(debate) + digests;
|
|
1427
|
+
|
|
1428
|
+
// Include debate history so moderator has context
|
|
1429
|
+
moderatorContext += "\n\nDebate history so far:\n---\n";
|
|
1430
|
+
for (var hi = 0; hi < debate.history.length; hi++) {
|
|
1431
|
+
var h = debate.history[hi];
|
|
1432
|
+
moderatorContext += (h.mateName || h.speaker || "Unknown") + " (" + (h.role || "") + "): " + (h.text || "").slice(0, 500) + "\n\n";
|
|
1433
|
+
}
|
|
1434
|
+
moderatorContext += "---\n";
|
|
1435
|
+
|
|
1436
|
+
ctx.sdk.createMentionSession({
|
|
1437
|
+
claudeMd: claudeMd,
|
|
1438
|
+
initialContext: moderatorContext,
|
|
1439
|
+
initialMessage: resumePrompt,
|
|
1440
|
+
onActivity: function (activity) {
|
|
1441
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
1442
|
+
ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
onDelta: function (delta) {
|
|
1446
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
1447
|
+
ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
onDone: function (fullText) {
|
|
1451
|
+
handleModeratorTurnDone(session, fullText);
|
|
1452
|
+
},
|
|
1453
|
+
onError: function (errMsg) {
|
|
1454
|
+
console.error("[debate] Moderator resume error:", errMsg);
|
|
1455
|
+
endDebate(session, "error");
|
|
1456
|
+
},
|
|
1457
|
+
canUseTool: buildDebateToolHandler(session),
|
|
1458
|
+
}).then(function (mentionSession) {
|
|
1459
|
+
if (mentionSession) {
|
|
1460
|
+
debate.moderatorSession = mentionSession;
|
|
1461
|
+
}
|
|
1462
|
+
}).catch(function (err) {
|
|
1463
|
+
console.error("[debate] Failed to create resume moderator session:", err.message || err);
|
|
1464
|
+
endDebate(session, "error");
|
|
1465
|
+
});
|
|
1466
|
+
} else {
|
|
1467
|
+
debate.moderatorSession.pushMessage(resumePrompt, buildModeratorCallbacks(session));
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// --- End debate ---
|
|
1474
|
+
|
|
1475
|
+
function endDebate(session, reason) {
|
|
1476
|
+
var debate = session._debate;
|
|
1477
|
+
if (!debate || debate.phase === "ended") return;
|
|
1478
|
+
|
|
1479
|
+
debate.phase = "ended";
|
|
1480
|
+
debate.turnInProgress = false;
|
|
1481
|
+
persistDebateState(session);
|
|
1482
|
+
|
|
1483
|
+
// Clean up brief watcher if still active
|
|
1484
|
+
if (debate._briefWatcher) {
|
|
1485
|
+
try { debate._briefWatcher.close(); } catch (e) {}
|
|
1486
|
+
debate._briefWatcher = null;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Notify clients
|
|
1490
|
+
ctx.sendToSession(session.localId, {
|
|
1491
|
+
type: "debate_ended",
|
|
1492
|
+
reason: reason,
|
|
1493
|
+
rounds: debate.round,
|
|
1494
|
+
topic: debate.topic,
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
// Save to session history
|
|
1498
|
+
var endEntry = { type: "debate_ended", topic: debate.topic, rounds: debate.round, reason: reason };
|
|
1499
|
+
session.history.push(endEntry);
|
|
1500
|
+
ctx.sm.appendToSessionFile(session, endEntry);
|
|
1501
|
+
|
|
1502
|
+
// Generate digests for all participants
|
|
1503
|
+
digestDebateParticipant(session, debate.moderatorId, debate, "moderator");
|
|
1504
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
1505
|
+
digestDebateParticipant(session, debate.panelists[i].mateId, debate, debate.panelists[i].role);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function digestDebateParticipant(session, mateId, debate, role) {
|
|
1510
|
+
var mentionSession = null;
|
|
1511
|
+
if (mateId === debate.moderatorId) {
|
|
1512
|
+
mentionSession = debate.moderatorSession;
|
|
1513
|
+
} else {
|
|
1514
|
+
mentionSession = debate.panelistSessions[mateId];
|
|
1515
|
+
}
|
|
1516
|
+
if (!mentionSession || !mentionSession.isAlive()) return;
|
|
1517
|
+
|
|
1518
|
+
var mateDir = matesModule.getMateDir(debate.mateCtx, mateId);
|
|
1519
|
+
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
1520
|
+
|
|
1521
|
+
// Migration: generate initial summary if missing
|
|
1522
|
+
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
1523
|
+
var digestFileCheck = path.join(knowledgeDir, "session-digests.jsonl");
|
|
1524
|
+
if (!fs.existsSync(summaryFile) && fs.existsSync(digestFileCheck)) {
|
|
1525
|
+
ctx.initMemorySummary(debate.mateCtx, mateId, function () {});
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Debates are user-initiated structured events. The moderator already
|
|
1529
|
+
// synthesizes a summary, so skip the memory gate and always create a digest.
|
|
1530
|
+
(function () {
|
|
1531
|
+
var digestPrompt = [
|
|
1532
|
+
"[SYSTEM: Session Digest]",
|
|
1533
|
+
"Summarize this conversation from YOUR perspective for your long-term memory.",
|
|
1534
|
+
"Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
|
|
1535
|
+
"",
|
|
1536
|
+
"Schema:",
|
|
1537
|
+
"{",
|
|
1538
|
+
' "date": "YYYY-MM-DD",',
|
|
1539
|
+
' "type": "debate",',
|
|
1540
|
+
' "topic": "short topic description",',
|
|
1541
|
+
' "my_position": "what I said/recommended",',
|
|
1542
|
+
' "decisions": "what was decided, or null if pending",',
|
|
1543
|
+
' "open_items": "what remains unresolved",',
|
|
1544
|
+
' "user_sentiment": "how the user seemed to feel",',
|
|
1545
|
+
' "my_role": "' + role + '",',
|
|
1546
|
+
' "other_perspectives": "key points from others",',
|
|
1547
|
+
' "outcome": "how the debate concluded",',
|
|
1548
|
+
' "confidence": "high | medium | low",',
|
|
1549
|
+
' "revisit_later": true/false,',
|
|
1550
|
+
' "tags": ["relevant", "topic", "tags"]',
|
|
1551
|
+
"}",
|
|
1552
|
+
"",
|
|
1553
|
+
"IMPORTANT: Output ONLY the JSON object. Nothing else.",
|
|
1554
|
+
].join("\n");
|
|
1555
|
+
|
|
1556
|
+
var digestText = "";
|
|
1557
|
+
mentionSession.pushMessage(digestPrompt, {
|
|
1558
|
+
onActivity: function () {},
|
|
1559
|
+
onDelta: function (delta) {
|
|
1560
|
+
digestText += delta;
|
|
1561
|
+
},
|
|
1562
|
+
onDone: function () {
|
|
1563
|
+
var digestObj = null;
|
|
1564
|
+
try {
|
|
1565
|
+
var cleaned = digestText.trim();
|
|
1566
|
+
if (cleaned.indexOf("```") === 0) {
|
|
1567
|
+
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
1568
|
+
}
|
|
1569
|
+
digestObj = JSON.parse(cleaned);
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
console.error("[debate-digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
|
|
1572
|
+
digestObj = {
|
|
1573
|
+
date: new Date().toISOString().slice(0, 10),
|
|
1574
|
+
type: "debate",
|
|
1575
|
+
topic: debate.topic,
|
|
1576
|
+
my_role: role,
|
|
1577
|
+
raw: digestText.substring(0, 500),
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
try {
|
|
1582
|
+
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
1583
|
+
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
1584
|
+
fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
console.error("[debate-digest] Failed to write digest for mate " + mateId + ":", e.message);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Update memory summary
|
|
1590
|
+
ctx.updateMemorySummary(debate.mateCtx, mateId, digestObj);
|
|
1591
|
+
|
|
1592
|
+
// Close the session after digest
|
|
1593
|
+
mentionSession.close();
|
|
1594
|
+
},
|
|
1595
|
+
onError: function (err) {
|
|
1596
|
+
console.error("[debate-digest] Digest generation failed for mate " + mateId + ":", err);
|
|
1597
|
+
mentionSession.close();
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
})();
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// --- Public API ---
|
|
1604
|
+
|
|
1605
|
+
return {
|
|
1606
|
+
handleDebateStart: handleDebateStart,
|
|
1607
|
+
handleDebateComment: handleDebateComment,
|
|
1608
|
+
handleDebateStop: handleDebateStop,
|
|
1609
|
+
handleDebateConcludeResponse: handleDebateConcludeResponse,
|
|
1610
|
+
handleDebateConfirmBrief: handleDebateConfirmBrief,
|
|
1611
|
+
restoreDebateState: restoreDebateState,
|
|
1612
|
+
checkForDmDebateBrief: checkForDmDebateBrief,
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
module.exports = { attachDebate: attachDebate };
|