clay-server 2.18.0-beta.9 → 2.18.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/project.js +1445 -32
- package/lib/public/app.js +309 -125
- package/lib/public/css/debate.css +1039 -0
- package/lib/public/css/mates.css +125 -0
- package/lib/public/css/scheduler-modal.css +110 -392
- package/lib/public/css/scheduler.css +10 -3
- package/lib/public/css/sidebar.css +80 -7
- package/lib/public/index.html +53 -106
- package/lib/public/modules/debate.js +633 -0
- package/lib/public/modules/input.js +14 -5
- package/lib/public/modules/mate-sidebar.js +169 -2
- package/lib/public/modules/mention.js +6 -3
- package/lib/public/modules/scheduler.js +373 -252
- package/lib/public/modules/sidebar.js +158 -28
- package/lib/public/modules/tools.js +13 -3
- package/lib/public/modules/tooltip.js +2 -0
- package/lib/public/style.css +1 -0
- package/lib/scheduler.js +59 -1
- package/lib/sessions.js +9 -2
- package/lib/user-presence.js +92 -0
- package/package.json +1 -1
package/lib/project.js
CHANGED
|
@@ -13,6 +13,7 @@ var usersModule = require("./users");
|
|
|
13
13
|
var { resolveOsUserInfo, fsAsUser } = require("./os-users");
|
|
14
14
|
var crisisSafety = require("./crisis-safety");
|
|
15
15
|
var matesModule = require("./mates");
|
|
16
|
+
var userPresence = require("./user-presence");
|
|
16
17
|
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
17
18
|
|
|
18
19
|
// Validate environment variable string (KEY=VALUE per line)
|
|
@@ -670,30 +671,31 @@ function createProjectContext(opts) {
|
|
|
670
671
|
}
|
|
671
672
|
|
|
672
673
|
// For schedule records, resolve the linked task to get loop files
|
|
673
|
-
var
|
|
674
|
+
var loopFilesId = record.id;
|
|
674
675
|
if (record.source === "schedule") {
|
|
675
676
|
if (!record.linkedTaskId) {
|
|
676
677
|
console.error("[loop-registry] Schedule has no linked task: " + record.name);
|
|
677
678
|
return;
|
|
678
679
|
}
|
|
679
|
-
|
|
680
|
-
console.log("[loop-registry] Schedule triggered: " + record.name + " → linked task " +
|
|
680
|
+
loopFilesId = record.linkedTaskId;
|
|
681
|
+
console.log("[loop-registry] Schedule triggered: " + record.name + " → linked task " + loopFilesId);
|
|
681
682
|
}
|
|
682
683
|
|
|
683
684
|
// Verify the loop directory and PROMPT.md exist
|
|
684
|
-
var recDir = path.join(cwd, ".claude", "loops",
|
|
685
|
+
var recDir = path.join(cwd, ".claude", "loops", loopFilesId);
|
|
685
686
|
try {
|
|
686
687
|
fs.accessSync(path.join(recDir, "PROMPT.md"));
|
|
687
688
|
} catch (e) {
|
|
688
|
-
console.error("[loop-registry] PROMPT.md missing for " +
|
|
689
|
+
console.error("[loop-registry] PROMPT.md missing for " + loopFilesId);
|
|
689
690
|
return;
|
|
690
691
|
}
|
|
691
|
-
// Set the loopId
|
|
692
|
-
loopState.loopId =
|
|
692
|
+
// Set the loopId to the schedule's own id (not the linked task) so sidebar groups correctly
|
|
693
|
+
loopState.loopId = record.id;
|
|
694
|
+
loopState.wizardData = null;
|
|
693
695
|
activeRegistryId = record.id;
|
|
694
696
|
console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopId + ")");
|
|
695
697
|
send({ type: "schedule_run_started", recordId: record.id });
|
|
696
|
-
startLoop({ maxIterations: record.maxIterations });
|
|
698
|
+
startLoop({ maxIterations: record.maxIterations, name: record.name });
|
|
697
699
|
},
|
|
698
700
|
onChange: function () {
|
|
699
701
|
send({ type: "loop_registry_updated", records: getHubSchedules() });
|
|
@@ -752,18 +754,19 @@ function createProjectContext(opts) {
|
|
|
752
754
|
loopState.promptText = promptText;
|
|
753
755
|
loopState.judgeText = judgeText;
|
|
754
756
|
loopState.iteration = 0;
|
|
755
|
-
loopState.maxIterations = judgeText ? (
|
|
757
|
+
loopState.maxIterations = judgeText ? ((loopOpts.maxIterations >= 1 ? loopOpts.maxIterations : null) || loopConfig.maxIterations || 20) : 1;
|
|
756
758
|
loopState.baseCommit = baseCommit;
|
|
757
759
|
loopState.currentSessionId = null;
|
|
758
760
|
loopState.judgeSessionId = null;
|
|
759
761
|
loopState.results = [];
|
|
760
762
|
loopState.stopping = false;
|
|
763
|
+
loopState.name = loopOpts.name || null;
|
|
761
764
|
loopState.startedAt = Date.now();
|
|
762
765
|
saveLoopState();
|
|
763
766
|
|
|
764
767
|
stopClaudeDirWatch();
|
|
765
768
|
|
|
766
|
-
send({ type: "loop_started", maxIterations: loopState.maxIterations });
|
|
769
|
+
send({ type: "loop_started", maxIterations: loopState.maxIterations, name: loopState.name });
|
|
767
770
|
runNextIteration();
|
|
768
771
|
}
|
|
769
772
|
|
|
@@ -781,8 +784,8 @@ function createProjectContext(opts) {
|
|
|
781
784
|
}
|
|
782
785
|
|
|
783
786
|
var session = sm.createSession();
|
|
784
|
-
var loopName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
785
787
|
var loopSource = loopRegistry.getById(loopState.loopId);
|
|
788
|
+
var loopName = (loopState.wizardData && loopState.wizardData.name) || (loopSource && loopSource.name) || "";
|
|
786
789
|
var loopSourceTag = (loopSource && loopSource.source) || null;
|
|
787
790
|
var isRalphLoop = loopSourceTag === "ralph";
|
|
788
791
|
session.loop = { active: true, iteration: loopState.iteration, role: "coder", loopId: loopState.loopId, name: loopName, source: loopSourceTag, startedAt: loopState.startedAt };
|
|
@@ -830,7 +833,7 @@ function createProjectContext(opts) {
|
|
|
830
833
|
setTimeout(function() { runNextIteration(); }, 2000);
|
|
831
834
|
return;
|
|
832
835
|
}
|
|
833
|
-
if (loopState.judgeText) {
|
|
836
|
+
if (loopState.judgeText && loopState.maxIterations > 1) {
|
|
834
837
|
runJudge();
|
|
835
838
|
} else {
|
|
836
839
|
finishLoop("pass");
|
|
@@ -909,8 +912,8 @@ function createProjectContext(opts) {
|
|
|
909
912
|
"- FAIL: [brief explanation of what is still missing]";
|
|
910
913
|
|
|
911
914
|
var judgeSession = sm.createSession();
|
|
912
|
-
var judgeName = (loopState.wizardData && loopState.wizardData.name) || "";
|
|
913
915
|
var judgeSource = loopRegistry.getById(loopState.loopId);
|
|
916
|
+
var judgeName = (loopState.wizardData && loopState.wizardData.name) || (judgeSource && judgeSource.name) || "";
|
|
914
917
|
var judgeSourceTag = (judgeSource && judgeSource.source) || null;
|
|
915
918
|
var isRalphJudge = judgeSourceTag === "ralph";
|
|
916
919
|
judgeSession.loop = { active: true, iteration: loopState.iteration, role: "judge", loopId: loopState.loopId, name: judgeName, source: judgeSourceTag, startedAt: loopState.startedAt };
|
|
@@ -1187,6 +1190,7 @@ function createProjectContext(opts) {
|
|
|
1187
1190
|
active: loopState.active,
|
|
1188
1191
|
iteration: loopState.iteration,
|
|
1189
1192
|
maxIterations: loopState.maxIterations,
|
|
1193
|
+
name: loopState.name || null,
|
|
1190
1194
|
});
|
|
1191
1195
|
|
|
1192
1196
|
// Ralph phase state
|
|
@@ -1247,14 +1251,26 @@ function createProjectContext(opts) {
|
|
|
1247
1251
|
}),
|
|
1248
1252
|
});
|
|
1249
1253
|
|
|
1250
|
-
// Restore active session for this client
|
|
1251
|
-
var active =
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
+
// Restore active session for this client from server-side presence
|
|
1255
|
+
var active = null;
|
|
1256
|
+
var presenceKey = wsUser ? wsUser.id : "_default";
|
|
1257
|
+
var storedPresence = userPresence.getPresence(slug, presenceKey);
|
|
1258
|
+
if (storedPresence && storedPresence.sessionId) {
|
|
1259
|
+
// Look up stored session by localId
|
|
1260
|
+
if (sm.sessions.has(storedPresence.sessionId)) {
|
|
1261
|
+
active = sm.sessions.get(storedPresence.sessionId);
|
|
1262
|
+
} else {
|
|
1263
|
+
// Try matching by cliSessionId (survives server restarts where localIds change)
|
|
1264
|
+
sm.sessions.forEach(function (s) {
|
|
1265
|
+
if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
// Validate access
|
|
1269
|
+
if (active && usersModule.isMultiUser() && wsUser) {
|
|
1270
|
+
if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
|
|
1271
|
+
} else if (active && !usersModule.isMultiUser() && active.ownerId) {
|
|
1254
1272
|
active = null;
|
|
1255
1273
|
}
|
|
1256
|
-
} else if (active && !usersModule.isMultiUser() && active.ownerId) {
|
|
1257
|
-
active = null;
|
|
1258
1274
|
}
|
|
1259
1275
|
// Fallback: pick the most recent accessible session
|
|
1260
1276
|
if (!active && allSessions.length > 0) {
|
|
@@ -1269,13 +1285,13 @@ function createProjectContext(opts) {
|
|
|
1269
1285
|
var autoCreated = false;
|
|
1270
1286
|
if (!active) {
|
|
1271
1287
|
var autoOpts = {};
|
|
1272
|
-
if (wsUser) autoOpts.ownerId = wsUser.id;
|
|
1288
|
+
if (wsUser && usersModule.isMultiUser()) autoOpts.ownerId = wsUser.id;
|
|
1273
1289
|
active = sm.createSession(autoOpts, ws);
|
|
1274
1290
|
autoCreated = true;
|
|
1275
1291
|
}
|
|
1276
1292
|
if (active && !autoCreated) {
|
|
1277
|
-
// Backfill ownerId for legacy sessions restored without one
|
|
1278
|
-
if (!active.ownerId && wsUser) {
|
|
1293
|
+
// Backfill ownerId for legacy sessions restored without one (multi-user only)
|
|
1294
|
+
if (!active.ownerId && wsUser && usersModule.isMultiUser()) {
|
|
1279
1295
|
active.ownerId = wsUser.id;
|
|
1280
1296
|
sm.saveSessionFile(active);
|
|
1281
1297
|
}
|
|
@@ -1289,7 +1305,11 @@ function createProjectContext(opts) {
|
|
|
1289
1305
|
}
|
|
1290
1306
|
sendTo(ws, { type: "history_meta", total: total, from: fromIndex });
|
|
1291
1307
|
for (var i = fromIndex; i < total; i++) {
|
|
1292
|
-
|
|
1308
|
+
var _hitem = active.history[i];
|
|
1309
|
+
if (_hitem && (_hitem.type === "mention_user" || _hitem.type === "mention_response")) {
|
|
1310
|
+
console.log("[DEBUG handleConnection] sending mention at index=" + i + " from=" + fromIndex + " total=" + total + " type=" + _hitem.type + " mate=" + (_hitem.mateName || "") + " slug=" + slug);
|
|
1311
|
+
}
|
|
1312
|
+
sendTo(ws, hydrateImageRefs(_hitem));
|
|
1293
1313
|
}
|
|
1294
1314
|
sendTo(ws, { type: "history_done" });
|
|
1295
1315
|
|
|
@@ -1306,12 +1326,24 @@ function createProjectContext(opts) {
|
|
|
1306
1326
|
toolInput: p.toolInput,
|
|
1307
1327
|
toolUseId: p.toolUseId,
|
|
1308
1328
|
decisionReason: p.decisionReason,
|
|
1329
|
+
mateId: p.mateId || undefined,
|
|
1309
1330
|
});
|
|
1310
1331
|
}
|
|
1311
1332
|
}
|
|
1312
1333
|
|
|
1334
|
+
// Record presence for this user + send mate DM restore hint if applicable
|
|
1335
|
+
if (active) {
|
|
1336
|
+
userPresence.setPresence(slug, presenceKey, active.localId, storedPresence ? storedPresence.mateDm : null);
|
|
1337
|
+
}
|
|
1338
|
+
if (storedPresence && storedPresence.mateDm) {
|
|
1339
|
+
sendTo(ws, { type: "restore_mate_dm", mateId: storedPresence.mateDm });
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1313
1342
|
broadcastPresence();
|
|
1314
1343
|
|
|
1344
|
+
// Restore debate state and brief watcher if a debate was in progress
|
|
1345
|
+
restoreDebateState(ws);
|
|
1346
|
+
|
|
1315
1347
|
ws.on("message", function (raw) {
|
|
1316
1348
|
var msg;
|
|
1317
1349
|
try { msg = JSON.parse(raw.toString()); } catch (e) { return; }
|
|
@@ -1343,6 +1375,24 @@ function createProjectContext(opts) {
|
|
|
1343
1375
|
return;
|
|
1344
1376
|
}
|
|
1345
1377
|
|
|
1378
|
+
// --- Debate ---
|
|
1379
|
+
if (msg.type === "debate_start") {
|
|
1380
|
+
handleDebateStart(ws, msg);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (msg.type === "debate_comment") {
|
|
1384
|
+
handleDebateComment(ws, msg);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (msg.type === "debate_stop") {
|
|
1388
|
+
handleDebateStop(ws);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (msg.type === "debate_conclude_response") {
|
|
1392
|
+
handleDebateConcludeResponse(ws, msg);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1346
1396
|
// --- Knowledge file management ---
|
|
1347
1397
|
if (msg.type === "knowledge_list") {
|
|
1348
1398
|
var knowledgeDir = path.join(cwd, "knowledge");
|
|
@@ -1526,10 +1576,12 @@ function createProjectContext(opts) {
|
|
|
1526
1576
|
|
|
1527
1577
|
if (msg.type === "new_session") {
|
|
1528
1578
|
var sessionOpts = {};
|
|
1529
|
-
if (ws._clayUser) sessionOpts.ownerId = ws._clayUser.id;
|
|
1579
|
+
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
1530
1580
|
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
1531
1581
|
var newSess = sm.createSession(sessionOpts, ws);
|
|
1532
1582
|
ws._clayActiveSession = newSess.localId;
|
|
1583
|
+
var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
1584
|
+
userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
|
|
1533
1585
|
if (usersModule.isMultiUser()) {
|
|
1534
1586
|
broadcastPresence();
|
|
1535
1587
|
}
|
|
@@ -1660,10 +1712,18 @@ function createProjectContext(opts) {
|
|
|
1660
1712
|
ws._clayActiveSession = msg.id;
|
|
1661
1713
|
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
1662
1714
|
}
|
|
1715
|
+
var swPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
1716
|
+
userPresence.setPresence(slug, swPresKey, msg.id, null);
|
|
1663
1717
|
}
|
|
1664
1718
|
return;
|
|
1665
1719
|
}
|
|
1666
1720
|
|
|
1721
|
+
if (msg.type === "set_mate_dm") {
|
|
1722
|
+
var dmPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
1723
|
+
userPresence.setMateDm(slug, dmPresKey, msg.mateId || null);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1667
1727
|
if (msg.type === "delete_session") {
|
|
1668
1728
|
if (ws._clayUser) {
|
|
1669
1729
|
var sdPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
@@ -3320,6 +3380,7 @@ function createProjectContext(opts) {
|
|
|
3320
3380
|
color: sData.color || null,
|
|
3321
3381
|
recurrenceEnd: sData.recurrenceEnd || null,
|
|
3322
3382
|
skipIfRunning: sData.skipIfRunning !== undefined ? sData.skipIfRunning : true,
|
|
3383
|
+
intervalEnd: sData.intervalEnd || null,
|
|
3323
3384
|
});
|
|
3324
3385
|
return;
|
|
3325
3386
|
}
|
|
@@ -3424,8 +3485,8 @@ function createProjectContext(opts) {
|
|
|
3424
3485
|
var session = getSessionForWs(ws);
|
|
3425
3486
|
if (!session) return;
|
|
3426
3487
|
|
|
3427
|
-
// Backfill ownerId for legacy sessions restored without one
|
|
3428
|
-
if (!session.ownerId && ws._clayUser) {
|
|
3488
|
+
// Backfill ownerId for legacy sessions restored without one (multi-user only)
|
|
3489
|
+
if (!session.ownerId && ws._clayUser && usersModule.isMultiUser()) {
|
|
3429
3490
|
session.ownerId = ws._clayUser.id;
|
|
3430
3491
|
sm.saveSessionFile(session);
|
|
3431
3492
|
}
|
|
@@ -3499,7 +3560,7 @@ function createProjectContext(opts) {
|
|
|
3499
3560
|
}
|
|
3500
3561
|
|
|
3501
3562
|
// --- @Mention handler ---
|
|
3502
|
-
var MENTION_WINDOW =
|
|
3563
|
+
var MENTION_WINDOW = 20; // turns to check for session continuity
|
|
3503
3564
|
|
|
3504
3565
|
function getRecentTurns(session, n) {
|
|
3505
3566
|
var turns = [];
|
|
@@ -3655,6 +3716,12 @@ function createProjectContext(opts) {
|
|
|
3655
3716
|
var session = getSessionForWs(ws);
|
|
3656
3717
|
if (!session) return;
|
|
3657
3718
|
|
|
3719
|
+
// Block mentions during an active debate
|
|
3720
|
+
if (session._debate && session._debate.phase === "live") {
|
|
3721
|
+
sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Cannot use @mentions during an active debate." });
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3658
3725
|
// Check if a mention is already in progress for this session
|
|
3659
3726
|
if (session._mentionInProgress) {
|
|
3660
3727
|
sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "A mention is already in progress." });
|
|
@@ -3832,7 +3899,7 @@ function createProjectContext(opts) {
|
|
|
3832
3899
|
onDone: mentionCallbacks.onDone,
|
|
3833
3900
|
onError: mentionCallbacks.onError,
|
|
3834
3901
|
canUseTool: function (toolName, input, toolOpts) {
|
|
3835
|
-
var autoAllow = { Read: true, Glob: true, Grep: true };
|
|
3902
|
+
var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
3836
3903
|
if (autoAllow[toolName]) {
|
|
3837
3904
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
3838
3905
|
}
|
|
@@ -3846,6 +3913,7 @@ function createProjectContext(opts) {
|
|
|
3846
3913
|
toolInput: input,
|
|
3847
3914
|
toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
|
|
3848
3915
|
decisionReason: (toolOpts && toolOpts.decisionReason) || "",
|
|
3916
|
+
mateId: msg.mateId,
|
|
3849
3917
|
};
|
|
3850
3918
|
sendToSession(session.localId, {
|
|
3851
3919
|
type: "permission_request",
|
|
@@ -3854,6 +3922,7 @@ function createProjectContext(opts) {
|
|
|
3854
3922
|
toolInput: input,
|
|
3855
3923
|
toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
|
|
3856
3924
|
decisionReason: (toolOpts && toolOpts.decisionReason) || "",
|
|
3925
|
+
mateId: msg.mateId,
|
|
3857
3926
|
});
|
|
3858
3927
|
onProcessingChanged();
|
|
3859
3928
|
if (toolOpts && toolOpts.signal) {
|
|
@@ -3878,6 +3947,1313 @@ function createProjectContext(opts) {
|
|
|
3878
3947
|
}
|
|
3879
3948
|
}
|
|
3880
3949
|
|
|
3950
|
+
// --- Debate engine ---
|
|
3951
|
+
|
|
3952
|
+
function escapeRegex(str) {
|
|
3953
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
function buildDebateNameMap(panelists, mateCtx) {
|
|
3957
|
+
var nameMap = {};
|
|
3958
|
+
for (var i = 0; i < panelists.length; i++) {
|
|
3959
|
+
var mate = matesModule.getMate(mateCtx, panelists[i].mateId);
|
|
3960
|
+
if (!mate) continue;
|
|
3961
|
+
var name = (mate.profile && mate.profile.displayName) || mate.name || "";
|
|
3962
|
+
if (name) {
|
|
3963
|
+
nameMap[name] = panelists[i].mateId;
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
return nameMap;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
function detectMentions(text, nameMap) {
|
|
3970
|
+
var names = Object.keys(nameMap);
|
|
3971
|
+
// Sort by length descending to match longest name first
|
|
3972
|
+
names.sort(function (a, b) { return b.length - a.length; });
|
|
3973
|
+
var mentioned = [];
|
|
3974
|
+
console.log("[debate-mention] nameMap keys:", JSON.stringify(names));
|
|
3975
|
+
console.log("[debate-mention] text snippet:", text.slice(0, 200));
|
|
3976
|
+
for (var i = 0; i < names.length; i++) {
|
|
3977
|
+
var pattern = new RegExp("@" + escapeRegex(names[i]) + "(?=[\\s,.:;!?()\\]}>\"']|$)", "i");
|
|
3978
|
+
var matched = pattern.test(text);
|
|
3979
|
+
console.log("[debate-mention] testing @" + names[i] + " pattern=" + pattern.toString() + " matched=" + matched);
|
|
3980
|
+
if (matched) {
|
|
3981
|
+
var mateId = nameMap[names[i]];
|
|
3982
|
+
if (mentioned.indexOf(mateId) === -1) {
|
|
3983
|
+
mentioned.push(mateId);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
return mentioned;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
function getMateProfile(mateCtx, mateId) {
|
|
3991
|
+
var mate = matesModule.getMate(mateCtx, mateId);
|
|
3992
|
+
if (!mate) return { name: "Mate", avatarColor: "#6c5ce7", avatarStyle: "bottts", avatarSeed: mateId };
|
|
3993
|
+
return {
|
|
3994
|
+
name: (mate.profile && mate.profile.displayName) || mate.name || "Mate",
|
|
3995
|
+
avatarColor: (mate.profile && mate.profile.avatarColor) || "#6c5ce7",
|
|
3996
|
+
avatarStyle: (mate.profile && mate.profile.avatarStyle) || "bottts",
|
|
3997
|
+
avatarSeed: (mate.profile && mate.profile.avatarSeed) || mateId,
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
function loadMateClaudeMd(mateCtx, mateId) {
|
|
4002
|
+
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4003
|
+
try {
|
|
4004
|
+
return fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
|
|
4005
|
+
} catch (e) {
|
|
4006
|
+
return "";
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
function loadMateDigests(mateCtx, mateId) {
|
|
4011
|
+
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4012
|
+
try {
|
|
4013
|
+
var digestFile = path.join(mateDir, "knowledge", "session-digests.jsonl");
|
|
4014
|
+
if (!fs.existsSync(digestFile)) return "";
|
|
4015
|
+
var allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n");
|
|
4016
|
+
var recent = allLines.slice(-5);
|
|
4017
|
+
if (recent.length === 0) return "";
|
|
4018
|
+
var lines = ["\n\nYour recent session memories:"];
|
|
4019
|
+
for (var i = 0; i < recent.length; i++) {
|
|
4020
|
+
try {
|
|
4021
|
+
var d = JSON.parse(recent[i]);
|
|
4022
|
+
lines.push("- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
|
|
4023
|
+
(d.decisions ? " | Decisions: " + d.decisions : "") +
|
|
4024
|
+
(d.open_items ? " | Open: " + d.open_items : "") );
|
|
4025
|
+
} catch (e) {}
|
|
4026
|
+
}
|
|
4027
|
+
return lines.join("\n");
|
|
4028
|
+
} catch (e) {
|
|
4029
|
+
return "";
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
function buildModeratorContext(debate) {
|
|
4034
|
+
var lines = [
|
|
4035
|
+
"You are moderating a structured debate among your AI teammates.",
|
|
4036
|
+
"",
|
|
4037
|
+
"Topic: " + debate.topic,
|
|
4038
|
+
"Format: " + debate.format,
|
|
4039
|
+
"Context: " + debate.context,
|
|
4040
|
+
];
|
|
4041
|
+
if (debate.specialRequests) {
|
|
4042
|
+
lines.push("Special requests: " + debate.specialRequests);
|
|
4043
|
+
}
|
|
4044
|
+
lines.push("");
|
|
4045
|
+
lines.push("Panelists:");
|
|
4046
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
4047
|
+
var p = debate.panelists[i];
|
|
4048
|
+
var profile = getMateProfile(debate.mateCtx, p.mateId);
|
|
4049
|
+
lines.push("- @" + profile.name + " (" + p.role + "): " + p.brief);
|
|
4050
|
+
}
|
|
4051
|
+
lines.push("");
|
|
4052
|
+
lines.push("RULES:");
|
|
4053
|
+
lines.push("1. To call on a panelist, mention them with @TheirName in your response.");
|
|
4054
|
+
lines.push("2. Only mention ONE panelist per response. Wait for their answer before calling the next.");
|
|
4055
|
+
lines.push("3. When you mention a panelist, clearly state what you want them to address.");
|
|
4056
|
+
lines.push("4. After hearing from all panelists, you may start additional rounds.");
|
|
4057
|
+
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.");
|
|
4058
|
+
lines.push("6. If the user interjects with a comment, acknowledge it and weave it into the discussion.");
|
|
4059
|
+
lines.push("");
|
|
4060
|
+
lines.push("Begin by introducing the topic and calling on the first panelist.");
|
|
4061
|
+
return lines.join("\n");
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
function buildPanelistContext(debate, panelistInfo) {
|
|
4065
|
+
var moderatorProfile = getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
4066
|
+
var lines = [
|
|
4067
|
+
"You are participating in a structured debate as a panelist.",
|
|
4068
|
+
"",
|
|
4069
|
+
"Topic: " + debate.topic,
|
|
4070
|
+
"Your role: " + panelistInfo.role,
|
|
4071
|
+
"Your brief: " + panelistInfo.brief,
|
|
4072
|
+
"",
|
|
4073
|
+
"Other panelists:",
|
|
4074
|
+
];
|
|
4075
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
4076
|
+
var p = debate.panelists[i];
|
|
4077
|
+
if (p.mateId === panelistInfo.mateId) continue;
|
|
4078
|
+
var profile = getMateProfile(debate.mateCtx, p.mateId);
|
|
4079
|
+
lines.push("- @" + profile.name + " (" + p.role + "): " + p.brief);
|
|
4080
|
+
}
|
|
4081
|
+
lines.push("");
|
|
4082
|
+
lines.push("The moderator is @" + moderatorProfile.name + ". They will call on you when it is your turn.");
|
|
4083
|
+
lines.push("");
|
|
4084
|
+
lines.push("RULES:");
|
|
4085
|
+
lines.push("1. Stay in your assigned role and perspective.");
|
|
4086
|
+
lines.push("2. Respond to the specific question or prompt from the moderator.");
|
|
4087
|
+
lines.push("3. You may reference what other panelists have said.");
|
|
4088
|
+
lines.push("4. Keep responses focused and substantive. Do not ramble.");
|
|
4089
|
+
lines.push("5. You have read-only access to project files if needed to support your arguments.");
|
|
4090
|
+
return lines.join("\n");
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
// --- Debate brief watcher (reusable for initial start and session restoration) ---
|
|
4094
|
+
function startDebateBriefWatcher(session, debate, briefPath) {
|
|
4095
|
+
if (!briefPath) {
|
|
4096
|
+
console.error("[debate] No briefPath provided to watcher");
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
// Persist briefPath on debate so restoration can reuse it
|
|
4100
|
+
debate.briefPath = briefPath;
|
|
4101
|
+
var watchDir = path.dirname(briefPath);
|
|
4102
|
+
var briefFilename = path.basename(briefPath);
|
|
4103
|
+
|
|
4104
|
+
// Clean up any existing watcher
|
|
4105
|
+
if (debate._briefWatcher) {
|
|
4106
|
+
try { debate._briefWatcher.close(); } catch (e) {}
|
|
4107
|
+
debate._briefWatcher = null;
|
|
4108
|
+
}
|
|
4109
|
+
if (debate._briefDebounce) {
|
|
4110
|
+
clearTimeout(debate._briefDebounce);
|
|
4111
|
+
debate._briefDebounce = null;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
function checkDebateBrief() {
|
|
4115
|
+
try {
|
|
4116
|
+
var raw = fs.readFileSync(briefPath, "utf8");
|
|
4117
|
+
var brief = JSON.parse(raw);
|
|
4118
|
+
|
|
4119
|
+
// Stop watching
|
|
4120
|
+
if (debate._briefWatcher) { debate._briefWatcher.close(); debate._briefWatcher = null; }
|
|
4121
|
+
if (debate._briefDebounce) { clearTimeout(debate._briefDebounce); debate._briefDebounce = null; }
|
|
4122
|
+
|
|
4123
|
+
// Clean up the brief file
|
|
4124
|
+
try { fs.unlinkSync(briefPath); } catch (e) {}
|
|
4125
|
+
|
|
4126
|
+
// Apply brief to debate state
|
|
4127
|
+
debate.topic = brief.topic || debate.topic;
|
|
4128
|
+
debate.format = brief.format || debate.format;
|
|
4129
|
+
debate.context = brief.context || "";
|
|
4130
|
+
debate.specialRequests = brief.specialRequests || null;
|
|
4131
|
+
|
|
4132
|
+
// Update panelists with roles from the brief
|
|
4133
|
+
if (brief.panelists && brief.panelists.length) {
|
|
4134
|
+
for (var i = 0; i < brief.panelists.length; i++) {
|
|
4135
|
+
var bp = brief.panelists[i];
|
|
4136
|
+
for (var j = 0; j < debate.panelists.length; j++) {
|
|
4137
|
+
if (debate.panelists[j].mateId === bp.mateId) {
|
|
4138
|
+
debate.panelists[j].role = bp.role || "";
|
|
4139
|
+
debate.panelists[j].brief = bp.brief || "";
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
// Rebuild name map with updated roles
|
|
4146
|
+
var mateCtx = debate.mateCtx || matesModule.buildMateCtx(null);
|
|
4147
|
+
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
4148
|
+
|
|
4149
|
+
console.log("[debate] Brief picked up, transitioning to live. Topic:", debate.topic);
|
|
4150
|
+
|
|
4151
|
+
// Transition to live
|
|
4152
|
+
startDebateLive(session);
|
|
4153
|
+
} catch (e) {
|
|
4154
|
+
// File not ready yet or invalid JSON, keep watching
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
try {
|
|
4159
|
+
try { fs.mkdirSync(watchDir, { recursive: true }); } catch (e) {}
|
|
4160
|
+
debate._briefWatcher = fs.watch(watchDir, function (eventType, filename) {
|
|
4161
|
+
if (filename === briefFilename) {
|
|
4162
|
+
if (debate._briefDebounce) clearTimeout(debate._briefDebounce);
|
|
4163
|
+
debate._briefDebounce = setTimeout(checkDebateBrief, 300);
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
4166
|
+
debate._briefWatcher.on("error", function () {});
|
|
4167
|
+
console.log("[debate] Watching for " + briefFilename + " at " + watchDir);
|
|
4168
|
+
} catch (e) {
|
|
4169
|
+
console.error("[debate] Failed to watch " + watchDir + ":", e.message);
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// Check immediately in case the file already exists (server restart scenario)
|
|
4173
|
+
checkDebateBrief();
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// Restore debate state and brief watcher on WS reconnect (after server restart)
|
|
4177
|
+
function restoreDebateState(ws) {
|
|
4178
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
4179
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
4180
|
+
|
|
4181
|
+
sm.sessions.forEach(function (session) {
|
|
4182
|
+
// Already restored
|
|
4183
|
+
if (session._debate) return;
|
|
4184
|
+
|
|
4185
|
+
// Has persisted debate state?
|
|
4186
|
+
if (!session.debateState) return;
|
|
4187
|
+
|
|
4188
|
+
var phase = session.debateState.phase;
|
|
4189
|
+
if (phase !== "preparing" && phase !== "live") return;
|
|
4190
|
+
|
|
4191
|
+
// Restore _debate from persisted state
|
|
4192
|
+
var debate = restoreDebateFromState(session);
|
|
4193
|
+
if (!debate) return;
|
|
4194
|
+
|
|
4195
|
+
// Update mateCtx with the connected user's context
|
|
4196
|
+
debate.mateCtx = mateCtx;
|
|
4197
|
+
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
4198
|
+
|
|
4199
|
+
var moderatorProfile = getMateProfile(mateCtx, debate.moderatorId);
|
|
4200
|
+
|
|
4201
|
+
if (phase === "preparing") {
|
|
4202
|
+
var briefPath = debate.briefPath;
|
|
4203
|
+
if (!briefPath && debate.debateId) {
|
|
4204
|
+
briefPath = path.join(cwd, ".clay", "debates", debate.debateId, "brief.json");
|
|
4205
|
+
}
|
|
4206
|
+
if (!briefPath) return;
|
|
4207
|
+
|
|
4208
|
+
console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
|
|
4209
|
+
startDebateBriefWatcher(session, debate, briefPath);
|
|
4210
|
+
|
|
4211
|
+
// Send preparing sticky to the connected client
|
|
4212
|
+
sendTo(ws, {
|
|
4213
|
+
type: "debate_preparing",
|
|
4214
|
+
topic: debate.topic,
|
|
4215
|
+
moderatorId: debate.moderatorId,
|
|
4216
|
+
moderatorName: moderatorProfile.name,
|
|
4217
|
+
setupSessionId: debate.setupSessionId,
|
|
4218
|
+
panelists: debate.panelists.map(function (p) {
|
|
4219
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
4220
|
+
return { mateId: p.mateId, name: prof.name };
|
|
4221
|
+
}),
|
|
4222
|
+
});
|
|
4223
|
+
} else if (phase === "live") {
|
|
4224
|
+
console.log("[debate] Restoring debate (live). topic:", debate.topic, "awaitingConclude:", debate.awaitingConcludeConfirm);
|
|
4225
|
+
// Debate was live when server restarted. It can't resume AI turns,
|
|
4226
|
+
// but we can show the sticky and let user see history.
|
|
4227
|
+
sendTo(ws, {
|
|
4228
|
+
type: "debate_started",
|
|
4229
|
+
topic: debate.topic,
|
|
4230
|
+
format: debate.format,
|
|
4231
|
+
round: debate.round,
|
|
4232
|
+
moderatorId: debate.moderatorId,
|
|
4233
|
+
moderatorName: moderatorProfile.name,
|
|
4234
|
+
panelists: debate.panelists.map(function (p) {
|
|
4235
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
4236
|
+
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
4237
|
+
}),
|
|
4238
|
+
});
|
|
4239
|
+
// If moderator had concluded, re-send conclude confirm so client shows End/Continue UI
|
|
4240
|
+
if (debate.awaitingConcludeConfirm) {
|
|
4241
|
+
sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
// Persist debate state to session file (survives server restart)
|
|
4248
|
+
function persistDebateState(session) {
|
|
4249
|
+
if (!session._debate) return;
|
|
4250
|
+
var d = session._debate;
|
|
4251
|
+
session.debateState = {
|
|
4252
|
+
phase: d.phase,
|
|
4253
|
+
topic: d.topic,
|
|
4254
|
+
format: d.format,
|
|
4255
|
+
context: d.context || "",
|
|
4256
|
+
specialRequests: d.specialRequests || null,
|
|
4257
|
+
moderatorId: d.moderatorId,
|
|
4258
|
+
panelists: d.panelists.map(function (p) {
|
|
4259
|
+
return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
|
|
4260
|
+
}),
|
|
4261
|
+
briefPath: d.briefPath || null,
|
|
4262
|
+
debateId: d.debateId || null,
|
|
4263
|
+
setupSessionId: d.setupSessionId || null,
|
|
4264
|
+
setupStartedAt: d.setupStartedAt || null,
|
|
4265
|
+
round: d.round || 1,
|
|
4266
|
+
awaitingConcludeConfirm: !!d.awaitingConcludeConfirm,
|
|
4267
|
+
};
|
|
4268
|
+
sm.saveSessionFile(session);
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
// Restore _debate from persisted debateState
|
|
4272
|
+
function restoreDebateFromState(session) {
|
|
4273
|
+
var ds = session.debateState;
|
|
4274
|
+
if (!ds) return null;
|
|
4275
|
+
var userId = null; // Will be set when WS connects
|
|
4276
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
4277
|
+
var debate = {
|
|
4278
|
+
phase: ds.phase,
|
|
4279
|
+
topic: ds.topic,
|
|
4280
|
+
format: ds.format,
|
|
4281
|
+
context: ds.context || "",
|
|
4282
|
+
specialRequests: ds.specialRequests || null,
|
|
4283
|
+
moderatorId: ds.moderatorId,
|
|
4284
|
+
panelists: ds.panelists || [],
|
|
4285
|
+
mateCtx: mateCtx,
|
|
4286
|
+
moderatorSession: null,
|
|
4287
|
+
panelistSessions: {},
|
|
4288
|
+
nameMap: buildDebateNameMap(ds.panelists || [], mateCtx),
|
|
4289
|
+
turnInProgress: false,
|
|
4290
|
+
pendingComment: null,
|
|
4291
|
+
round: ds.round || 1,
|
|
4292
|
+
history: [],
|
|
4293
|
+
setupSessionId: ds.setupSessionId || null,
|
|
4294
|
+
debateId: ds.debateId || null,
|
|
4295
|
+
setupStartedAt: ds.setupStartedAt || null,
|
|
4296
|
+
briefPath: ds.briefPath || null,
|
|
4297
|
+
awaitingConcludeConfirm: !!ds.awaitingConcludeConfirm,
|
|
4298
|
+
};
|
|
4299
|
+
|
|
4300
|
+
// Fallback: if awaitingConcludeConfirm was not persisted, detect from history
|
|
4301
|
+
if (!debate.awaitingConcludeConfirm && ds.phase === "live") {
|
|
4302
|
+
var hasEnded = false;
|
|
4303
|
+
var hasConclude = false;
|
|
4304
|
+
var lastModText = null;
|
|
4305
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
4306
|
+
var h = session.history[i];
|
|
4307
|
+
if (h.type === "debate_ended") hasEnded = true;
|
|
4308
|
+
if (h.type === "debate_conclude_confirm") hasConclude = true;
|
|
4309
|
+
if (h.type === "debate_turn_done" && h.role === "moderator") lastModText = h.text || "";
|
|
4310
|
+
}
|
|
4311
|
+
if (!hasEnded && !hasConclude && lastModText !== null) {
|
|
4312
|
+
var mentions = detectMentions(lastModText, debate.nameMap);
|
|
4313
|
+
if (mentions.length === 0) {
|
|
4314
|
+
debate.awaitingConcludeConfirm = true;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
session._debate = debate;
|
|
4320
|
+
return debate;
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
function buildDebateToolHandler(session) {
|
|
4324
|
+
return function (toolName, input, toolOpts) {
|
|
4325
|
+
var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
4326
|
+
if (autoAllow[toolName]) {
|
|
4327
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
4328
|
+
}
|
|
4329
|
+
return Promise.resolve({
|
|
4330
|
+
behavior: "deny",
|
|
4331
|
+
message: "Read-only access during debate. You cannot make changes.",
|
|
4332
|
+
});
|
|
4333
|
+
};
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
function handleDebateStart(ws, msg) {
|
|
4337
|
+
var session = getSessionForWs(ws);
|
|
4338
|
+
if (!session) return;
|
|
4339
|
+
|
|
4340
|
+
if (!msg.moderatorId || !msg.topic || !msg.panelists || !msg.panelists.length) {
|
|
4341
|
+
sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic, panelists." });
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
if (session._debate && (session._debate.phase === "live" || session._debate.phase === "preparing")) {
|
|
4346
|
+
sendTo(ws, { type: "debate_error", error: "A debate is already in progress." });
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
// Block mentions during debate
|
|
4351
|
+
if (session._mentionInProgress) {
|
|
4352
|
+
sendTo(ws, { type: "debate_error", error: "A mention is in progress. Wait for it to finish." });
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
4357
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
4358
|
+
var moderatorProfile = getMateProfile(mateCtx, msg.moderatorId);
|
|
4359
|
+
|
|
4360
|
+
// --- Phase 1: Preparing (clay-debate-setup skill) ---
|
|
4361
|
+
var debate = {
|
|
4362
|
+
phase: "preparing",
|
|
4363
|
+
topic: msg.topic,
|
|
4364
|
+
format: "free_discussion",
|
|
4365
|
+
context: "",
|
|
4366
|
+
specialRequests: null,
|
|
4367
|
+
moderatorId: msg.moderatorId,
|
|
4368
|
+
panelists: msg.panelists,
|
|
4369
|
+
mateCtx: mateCtx,
|
|
4370
|
+
moderatorSession: null,
|
|
4371
|
+
panelistSessions: {},
|
|
4372
|
+
nameMap: buildDebateNameMap(msg.panelists, mateCtx),
|
|
4373
|
+
turnInProgress: false,
|
|
4374
|
+
pendingComment: null,
|
|
4375
|
+
round: 1,
|
|
4376
|
+
history: [],
|
|
4377
|
+
setupSessionId: null,
|
|
4378
|
+
};
|
|
4379
|
+
session._debate = debate;
|
|
4380
|
+
|
|
4381
|
+
// Create a new session for the setup skill (like Ralph crafting)
|
|
4382
|
+
var debateId = "debate_" + Date.now();
|
|
4383
|
+
var setupSession = sm.createSession();
|
|
4384
|
+
setupSession.title = "Debate Setup: " + msg.topic.slice(0, 40);
|
|
4385
|
+
setupSession.debateSetupMode = true;
|
|
4386
|
+
setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: msg.topic.slice(0, 40), source: "debate", startedAt: Date.now() };
|
|
4387
|
+
sm.saveSessionFile(setupSession);
|
|
4388
|
+
sm.switchSession(setupSession.localId, null, hydrateImageRefs);
|
|
4389
|
+
debate.setupSessionId = setupSession.localId;
|
|
4390
|
+
debate.debateId = debateId;
|
|
4391
|
+
debate.setupStartedAt = setupSession.loop.startedAt;
|
|
4392
|
+
|
|
4393
|
+
// Build panelist info for the skill prompt
|
|
4394
|
+
var panelistNames = msg.panelists.map(function (p) {
|
|
4395
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
4396
|
+
return prof.name || p.mateId;
|
|
4397
|
+
}).join(", ");
|
|
4398
|
+
|
|
4399
|
+
var debateDir = path.join(cwd, ".clay", "debates", debateId);
|
|
4400
|
+
try { fs.mkdirSync(debateDir, { recursive: true }); } catch (e) {}
|
|
4401
|
+
var briefPath = path.join(debateDir, "brief.json");
|
|
4402
|
+
console.log("[debate] cwd=" + cwd + " debateDir=" + debateDir + " briefPath=" + briefPath);
|
|
4403
|
+
|
|
4404
|
+
var craftingPrompt = "Use the /clay-debate-setup skill to prepare a structured debate. " +
|
|
4405
|
+
"You MUST invoke the clay-debate-setup skill. Do NOT start the debate yourself.\n\n" +
|
|
4406
|
+
"## Initial Topic\n" + msg.topic + "\n\n" +
|
|
4407
|
+
"## Moderator\n" + (moderatorProfile.name || msg.moderatorId) + "\n\n" +
|
|
4408
|
+
"## Selected Panelists\n" + msg.panelists.map(function (p) {
|
|
4409
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
4410
|
+
return "- " + (prof.name || p.mateId) + " (ID: " + p.mateId + ")";
|
|
4411
|
+
}).join("\n") + "\n\n" +
|
|
4412
|
+
"## Debate Brief Output Path\n" +
|
|
4413
|
+
"When the setup is complete, write the debate brief JSON to this EXACT absolute path:\n" +
|
|
4414
|
+
"`" + briefPath + "`\n" +
|
|
4415
|
+
"This is where the debate engine watches for the file. Do NOT write it anywhere else.\n\n" +
|
|
4416
|
+
"## Spoken Language\nKorean (unless user switches)";
|
|
4417
|
+
|
|
4418
|
+
// Persist debate state before starting watcher
|
|
4419
|
+
debate.briefPath = briefPath;
|
|
4420
|
+
persistDebateState(session);
|
|
4421
|
+
|
|
4422
|
+
// Watch for brief.json in the debate-specific directory
|
|
4423
|
+
startDebateBriefWatcher(session, debate, briefPath);
|
|
4424
|
+
|
|
4425
|
+
// Notify clients that we are in preparing phase (send to both original and setup session)
|
|
4426
|
+
var preparingMsg = {
|
|
4427
|
+
type: "debate_preparing",
|
|
4428
|
+
topic: debate.topic,
|
|
4429
|
+
moderatorId: debate.moderatorId,
|
|
4430
|
+
moderatorName: moderatorProfile.name,
|
|
4431
|
+
setupSessionId: setupSession.localId,
|
|
4432
|
+
panelists: debate.panelists.map(function (p) {
|
|
4433
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
4434
|
+
return { mateId: p.mateId, name: prof.name };
|
|
4435
|
+
}),
|
|
4436
|
+
};
|
|
4437
|
+
// Send directly to the requesting ws (session switch may not have propagated yet)
|
|
4438
|
+
sendTo(ws, preparingMsg);
|
|
4439
|
+
// Also broadcast to any other clients on either session
|
|
4440
|
+
sendToSession(session.localId, preparingMsg);
|
|
4441
|
+
sendToSession(setupSession.localId, preparingMsg);
|
|
4442
|
+
|
|
4443
|
+
// Start the setup skill session
|
|
4444
|
+
setupSession.history.push({ type: "user_message", text: craftingPrompt });
|
|
4445
|
+
sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt });
|
|
4446
|
+
sendToSession(setupSession.localId, { type: "user_message", text: craftingPrompt });
|
|
4447
|
+
setupSession.isProcessing = true;
|
|
4448
|
+
onProcessingChanged();
|
|
4449
|
+
setupSession.sentToolResults = {};
|
|
4450
|
+
sendToSession(setupSession.localId, { type: "status", status: "processing" });
|
|
4451
|
+
sdk.startQuery(setupSession, craftingPrompt, undefined, getLinuxUserForSession(setupSession));
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
function startDebateLive(session) {
|
|
4455
|
+
var debate = session._debate;
|
|
4456
|
+
if (!debate || debate.phase === "live") return;
|
|
4457
|
+
|
|
4458
|
+
debate.phase = "live";
|
|
4459
|
+
debate.turnInProgress = true;
|
|
4460
|
+
debate.round = 1;
|
|
4461
|
+
|
|
4462
|
+
var mateCtx = debate.mateCtx;
|
|
4463
|
+
var moderatorProfile = getMateProfile(mateCtx, debate.moderatorId);
|
|
4464
|
+
|
|
4465
|
+
// Create a dedicated debate session, grouped with the setup session
|
|
4466
|
+
var debateSession = sm.createSession();
|
|
4467
|
+
debateSession.title = debate.topic.slice(0, 50);
|
|
4468
|
+
debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
|
|
4469
|
+
// Assign cliSessionId manually so saveSessionFile works (no SDK query for debate sessions)
|
|
4470
|
+
if (!debateSession.cliSessionId) {
|
|
4471
|
+
debateSession.cliSessionId = require("crypto").randomUUID();
|
|
4472
|
+
}
|
|
4473
|
+
sm.saveSessionFile(debateSession);
|
|
4474
|
+
sm.switchSession(debateSession.localId, null, hydrateImageRefs);
|
|
4475
|
+
debate.liveSessionId = debateSession.localId;
|
|
4476
|
+
|
|
4477
|
+
// Move _debate to the new session so all debate logic uses it
|
|
4478
|
+
debateSession._debate = debate;
|
|
4479
|
+
delete session._debate;
|
|
4480
|
+
// Clear persisted state from setup session, persist on live session
|
|
4481
|
+
session.debateState = null;
|
|
4482
|
+
sm.saveSessionFile(session);
|
|
4483
|
+
persistDebateState(debateSession);
|
|
4484
|
+
|
|
4485
|
+
// Save to session history
|
|
4486
|
+
var debateStartEntry = {
|
|
4487
|
+
type: "debate_started",
|
|
4488
|
+
topic: debate.topic,
|
|
4489
|
+
format: debate.format,
|
|
4490
|
+
moderatorId: debate.moderatorId,
|
|
4491
|
+
moderatorName: moderatorProfile.name,
|
|
4492
|
+
panelists: debate.panelists.map(function (p) {
|
|
4493
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
4494
|
+
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
4495
|
+
}),
|
|
4496
|
+
};
|
|
4497
|
+
debateSession.history.push(debateStartEntry);
|
|
4498
|
+
sm.appendToSessionFile(debateSession, debateStartEntry);
|
|
4499
|
+
|
|
4500
|
+
// Notify clients (same data as history entry)
|
|
4501
|
+
sendToSession(debateSession.localId, debateStartEntry);
|
|
4502
|
+
|
|
4503
|
+
// Signal moderator's first turn
|
|
4504
|
+
sendToSession(debateSession.localId, {
|
|
4505
|
+
type: "debate_turn",
|
|
4506
|
+
mateId: debate.moderatorId,
|
|
4507
|
+
mateName: moderatorProfile.name,
|
|
4508
|
+
role: "moderator",
|
|
4509
|
+
round: debate.round,
|
|
4510
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
4511
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
4512
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
4513
|
+
});
|
|
4514
|
+
|
|
4515
|
+
// Create moderator mention session
|
|
4516
|
+
var claudeMd = loadMateClaudeMd(mateCtx, debate.moderatorId);
|
|
4517
|
+
var digests = loadMateDigests(mateCtx, debate.moderatorId);
|
|
4518
|
+
var moderatorContext = buildModeratorContext(debate) + digests;
|
|
4519
|
+
|
|
4520
|
+
sdk.createMentionSession({
|
|
4521
|
+
claudeMd: claudeMd,
|
|
4522
|
+
initialContext: moderatorContext,
|
|
4523
|
+
initialMessage: "Begin the debate on: " + debate.topic,
|
|
4524
|
+
onActivity: function (activity) {
|
|
4525
|
+
if (debateSession._debate && debateSession._debate.phase !== "ended") {
|
|
4526
|
+
sendToSession(debateSession.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
|
|
4527
|
+
}
|
|
4528
|
+
},
|
|
4529
|
+
onDelta: function (delta) {
|
|
4530
|
+
if (debateSession._debate && debateSession._debate.phase !== "ended") {
|
|
4531
|
+
sendToSession(debateSession.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
4532
|
+
}
|
|
4533
|
+
},
|
|
4534
|
+
onDone: function (fullText) {
|
|
4535
|
+
handleModeratorTurnDone(debateSession, fullText);
|
|
4536
|
+
},
|
|
4537
|
+
onError: function (errMsg) {
|
|
4538
|
+
console.error("[debate] Moderator error:", errMsg);
|
|
4539
|
+
endDebate(debateSession, "error");
|
|
4540
|
+
},
|
|
4541
|
+
canUseTool: buildDebateToolHandler(debateSession),
|
|
4542
|
+
}).then(function (mentionSession) {
|
|
4543
|
+
if (mentionSession) {
|
|
4544
|
+
debate.moderatorSession = mentionSession;
|
|
4545
|
+
}
|
|
4546
|
+
}).catch(function (err) {
|
|
4547
|
+
console.error("[debate] Failed to create moderator session:", err.message || err);
|
|
4548
|
+
endDebate(debateSession, "error");
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
function handleModeratorTurnDone(session, fullText) {
|
|
4553
|
+
var debate = session._debate;
|
|
4554
|
+
if (!debate || debate.phase === "ended") return;
|
|
4555
|
+
|
|
4556
|
+
debate.turnInProgress = false;
|
|
4557
|
+
|
|
4558
|
+
// Record in debate history
|
|
4559
|
+
var moderatorProfile = getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
4560
|
+
debate.history.push({ speaker: "moderator", mateId: debate.moderatorId, mateName: moderatorProfile.name, text: fullText });
|
|
4561
|
+
|
|
4562
|
+
// Save to session history
|
|
4563
|
+
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 };
|
|
4564
|
+
session.history.push(turnEntry);
|
|
4565
|
+
sm.appendToSessionFile(session, turnEntry);
|
|
4566
|
+
sendToSession(session.localId, turnEntry);
|
|
4567
|
+
|
|
4568
|
+
// Check if user stopped the debate during this turn
|
|
4569
|
+
if (debate.phase === "ending") {
|
|
4570
|
+
endDebate(session, "user_stopped");
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
// Detect @mentions
|
|
4575
|
+
console.log("[debate] nameMap keys:", JSON.stringify(Object.keys(debate.nameMap)));
|
|
4576
|
+
console.log("[debate] moderator text (last 200):", fullText.slice(-200));
|
|
4577
|
+
var mentionedIds = detectMentions(fullText, debate.nameMap);
|
|
4578
|
+
console.log("[debate] detected mentions:", JSON.stringify(mentionedIds));
|
|
4579
|
+
|
|
4580
|
+
if (mentionedIds.length === 0) {
|
|
4581
|
+
// No mentions = moderator wants to conclude. Ask user to confirm.
|
|
4582
|
+
console.log("[debate] No mentions detected, requesting user confirmation to end.");
|
|
4583
|
+
debate.turnInProgress = false;
|
|
4584
|
+
debate.awaitingConcludeConfirm = true;
|
|
4585
|
+
var concludeEntry = { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round };
|
|
4586
|
+
session.history.push(concludeEntry);
|
|
4587
|
+
sm.appendToSessionFile(session, concludeEntry);
|
|
4588
|
+
sendToSession(session.localId, concludeEntry);
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
// Check for pending user comment before triggering panelist
|
|
4593
|
+
if (debate.pendingComment) {
|
|
4594
|
+
injectUserComment(session);
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
// Trigger the first mentioned panelist
|
|
4599
|
+
triggerPanelist(session, mentionedIds[0], fullText);
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
function triggerPanelist(session, mateId, moderatorText) {
|
|
4603
|
+
var debate = session._debate;
|
|
4604
|
+
if (!debate || debate.phase === "ended") return;
|
|
4605
|
+
|
|
4606
|
+
debate.turnInProgress = true;
|
|
4607
|
+
debate._currentTurnMateId = mateId;
|
|
4608
|
+
debate._currentTurnText = "";
|
|
4609
|
+
|
|
4610
|
+
var profile = getMateProfile(debate.mateCtx, mateId);
|
|
4611
|
+
var panelistInfo = null;
|
|
4612
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
4613
|
+
if (debate.panelists[i].mateId === mateId) {
|
|
4614
|
+
panelistInfo = debate.panelists[i];
|
|
4615
|
+
break;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
if (!panelistInfo) {
|
|
4619
|
+
console.error("[debate] Panelist not found:", mateId);
|
|
4620
|
+
debate._currentTurnMateId = null;
|
|
4621
|
+
// Feed error back to moderator
|
|
4622
|
+
feedBackToModerator(session, mateId, "[This panelist is not part of the debate panel.]");
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
// Notify clients of new turn
|
|
4627
|
+
sendToSession(session.localId, {
|
|
4628
|
+
type: "debate_turn",
|
|
4629
|
+
mateId: mateId,
|
|
4630
|
+
mateName: profile.name,
|
|
4631
|
+
role: panelistInfo.role,
|
|
4632
|
+
round: debate.round,
|
|
4633
|
+
avatarColor: profile.avatarColor,
|
|
4634
|
+
avatarStyle: profile.avatarStyle,
|
|
4635
|
+
avatarSeed: profile.avatarSeed,
|
|
4636
|
+
});
|
|
4637
|
+
|
|
4638
|
+
var panelistCallbacks = {
|
|
4639
|
+
onActivity: function (activity) {
|
|
4640
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
4641
|
+
sendToSession(session.localId, { type: "debate_activity", mateId: mateId, activity: activity });
|
|
4642
|
+
}
|
|
4643
|
+
},
|
|
4644
|
+
onDelta: function (delta) {
|
|
4645
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
4646
|
+
debate._currentTurnText += delta;
|
|
4647
|
+
sendToSession(session.localId, { type: "debate_stream", mateId: mateId, mateName: profile.name, delta: delta });
|
|
4648
|
+
}
|
|
4649
|
+
},
|
|
4650
|
+
onDone: function (fullText) {
|
|
4651
|
+
handlePanelistTurnDone(session, mateId, fullText);
|
|
4652
|
+
},
|
|
4653
|
+
onError: function (errMsg) {
|
|
4654
|
+
console.error("[debate] Panelist error for " + mateId + ":", errMsg);
|
|
4655
|
+
debate.turnInProgress = false;
|
|
4656
|
+
// Feed error back to moderator so the debate can continue
|
|
4657
|
+
feedBackToModerator(session, mateId, "[" + profile.name + " encountered an error and could not respond. Please continue with other panelists or wrap up.]");
|
|
4658
|
+
},
|
|
4659
|
+
};
|
|
4660
|
+
|
|
4661
|
+
// Check for existing session
|
|
4662
|
+
var existing = debate.panelistSessions[mateId];
|
|
4663
|
+
if (existing && existing.isAlive()) {
|
|
4664
|
+
// Build recent debate context for continuation
|
|
4665
|
+
var recentHistory = "";
|
|
4666
|
+
var lastPanelistIdx = -1;
|
|
4667
|
+
for (var hi = debate.history.length - 1; hi >= 0; hi--) {
|
|
4668
|
+
if (debate.history[hi].mateId === mateId) {
|
|
4669
|
+
lastPanelistIdx = hi;
|
|
4670
|
+
break;
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
if (lastPanelistIdx >= 0 && lastPanelistIdx < debate.history.length - 1) {
|
|
4674
|
+
recentHistory = "\n\n[Debate turns since your last response:]\n---\n";
|
|
4675
|
+
for (var hj = lastPanelistIdx + 1; hj < debate.history.length; hj++) {
|
|
4676
|
+
var h = debate.history[hj];
|
|
4677
|
+
recentHistory += h.mateName + " (" + (h.speaker === "moderator" ? "moderator" : h.role || h.speaker) + "): " + h.text.substring(0, 500) + "\n\n";
|
|
4678
|
+
}
|
|
4679
|
+
recentHistory += "---";
|
|
4680
|
+
}
|
|
4681
|
+
var continuationMsg = recentHistory + "\n\n[The moderator is now addressing you. Please respond.]\n\nModerator said:\n" + moderatorText;
|
|
4682
|
+
existing.pushMessage(continuationMsg, panelistCallbacks);
|
|
4683
|
+
} else {
|
|
4684
|
+
// Create new panelist session
|
|
4685
|
+
var claudeMd = loadMateClaudeMd(debate.mateCtx, mateId);
|
|
4686
|
+
var digests = loadMateDigests(debate.mateCtx, mateId);
|
|
4687
|
+
var panelistContext = buildPanelistContext(debate, panelistInfo) + digests;
|
|
4688
|
+
|
|
4689
|
+
// Include debate history so far for context
|
|
4690
|
+
var historyContext = "";
|
|
4691
|
+
if (debate.history.length > 0) {
|
|
4692
|
+
historyContext = "\n\n[Debate so far:]\n---\n";
|
|
4693
|
+
for (var hk = 0; hk < debate.history.length; hk++) {
|
|
4694
|
+
var he = debate.history[hk];
|
|
4695
|
+
historyContext += he.mateName + " (" + (he.speaker === "moderator" ? "moderator" : he.role || he.speaker) + "): " + he.text.substring(0, 500) + "\n\n";
|
|
4696
|
+
}
|
|
4697
|
+
historyContext += "---";
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
sdk.createMentionSession({
|
|
4701
|
+
claudeMd: claudeMd,
|
|
4702
|
+
initialContext: panelistContext + historyContext,
|
|
4703
|
+
initialMessage: "The moderator addresses you:\n\n" + moderatorText,
|
|
4704
|
+
onActivity: panelistCallbacks.onActivity,
|
|
4705
|
+
onDelta: panelistCallbacks.onDelta,
|
|
4706
|
+
onDone: panelistCallbacks.onDone,
|
|
4707
|
+
onError: panelistCallbacks.onError,
|
|
4708
|
+
canUseTool: buildDebateToolHandler(session),
|
|
4709
|
+
}).then(function (mentionSession) {
|
|
4710
|
+
if (mentionSession) {
|
|
4711
|
+
debate.panelistSessions[mateId] = mentionSession;
|
|
4712
|
+
}
|
|
4713
|
+
}).catch(function (err) {
|
|
4714
|
+
console.error("[debate] Failed to create panelist session for " + mateId + ":", err.message || err);
|
|
4715
|
+
debate.turnInProgress = false;
|
|
4716
|
+
feedBackToModerator(session, mateId, "[" + profile.name + " is unavailable. Please continue with other panelists or wrap up.]");
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
function handlePanelistTurnDone(session, mateId, fullText) {
|
|
4722
|
+
var debate = session._debate;
|
|
4723
|
+
if (!debate || debate.phase === "ended") return;
|
|
4724
|
+
|
|
4725
|
+
debate.turnInProgress = false;
|
|
4726
|
+
debate._currentTurnMateId = null;
|
|
4727
|
+
debate._currentTurnText = "";
|
|
4728
|
+
|
|
4729
|
+
var profile = getMateProfile(debate.mateCtx, mateId);
|
|
4730
|
+
var panelistInfo = null;
|
|
4731
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
4732
|
+
if (debate.panelists[i].mateId === mateId) {
|
|
4733
|
+
panelistInfo = debate.panelists[i];
|
|
4734
|
+
break;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
// Record in debate history
|
|
4739
|
+
debate.history.push({ speaker: "panelist", mateId: mateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: fullText });
|
|
4740
|
+
|
|
4741
|
+
// Save to session history
|
|
4742
|
+
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 };
|
|
4743
|
+
session.history.push(turnEntry);
|
|
4744
|
+
sm.appendToSessionFile(session, turnEntry);
|
|
4745
|
+
sendToSession(session.localId, turnEntry);
|
|
4746
|
+
|
|
4747
|
+
// Check if user stopped the debate
|
|
4748
|
+
if (debate.phase === "ending") {
|
|
4749
|
+
endDebate(session, "user_stopped");
|
|
4750
|
+
return;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
// Check for pending user comment
|
|
4754
|
+
if (debate.pendingComment) {
|
|
4755
|
+
injectUserComment(session);
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
// Feed panelist response back to moderator
|
|
4760
|
+
feedBackToModerator(session, mateId, fullText);
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
function feedBackToModerator(session, panelistMateId, panelistText) {
|
|
4764
|
+
var debate = session._debate;
|
|
4765
|
+
if (!debate || !debate.moderatorSession || debate.phase === "ended") return;
|
|
4766
|
+
|
|
4767
|
+
debate.round++;
|
|
4768
|
+
debate.turnInProgress = true;
|
|
4769
|
+
|
|
4770
|
+
var panelistProfile = getMateProfile(debate.mateCtx, panelistMateId);
|
|
4771
|
+
var panelistInfo = null;
|
|
4772
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
4773
|
+
if (debate.panelists[i].mateId === panelistMateId) {
|
|
4774
|
+
panelistInfo = debate.panelists[i];
|
|
4775
|
+
break;
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4779
|
+
var moderatorProfile = getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
4780
|
+
|
|
4781
|
+
// Notify clients of moderator turn
|
|
4782
|
+
sendToSession(session.localId, {
|
|
4783
|
+
type: "debate_turn",
|
|
4784
|
+
mateId: debate.moderatorId,
|
|
4785
|
+
mateName: moderatorProfile.name,
|
|
4786
|
+
role: "moderator",
|
|
4787
|
+
round: debate.round,
|
|
4788
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
4789
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
4790
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
4791
|
+
});
|
|
4792
|
+
|
|
4793
|
+
var feedText = "[Panelist Response]\n\n" +
|
|
4794
|
+
"@" + panelistProfile.name + " (" + (panelistInfo ? panelistInfo.role : "panelist") + ") responded:\n" +
|
|
4795
|
+
panelistText + "\n\n" +
|
|
4796
|
+
"Continue the debate. Call on the next panelist with @TheirName, or provide a closing summary (without any @mentions) to end the debate.";
|
|
4797
|
+
|
|
4798
|
+
debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
function buildModeratorCallbacks(session) {
|
|
4802
|
+
var debate = session._debate;
|
|
4803
|
+
var moderatorProfile = getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
4804
|
+
return {
|
|
4805
|
+
onActivity: function (activity) {
|
|
4806
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
4807
|
+
sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
|
|
4808
|
+
}
|
|
4809
|
+
},
|
|
4810
|
+
onDelta: function (delta) {
|
|
4811
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
4812
|
+
sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
4813
|
+
}
|
|
4814
|
+
},
|
|
4815
|
+
onDone: function (fullText) {
|
|
4816
|
+
handleModeratorTurnDone(session, fullText);
|
|
4817
|
+
},
|
|
4818
|
+
onError: function (errMsg) {
|
|
4819
|
+
console.error("[debate] Moderator error:", errMsg);
|
|
4820
|
+
endDebate(session, "error");
|
|
4821
|
+
},
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
function handleDebateComment(ws, msg) {
|
|
4826
|
+
var session = getSessionForWs(ws);
|
|
4827
|
+
if (!session) return;
|
|
4828
|
+
|
|
4829
|
+
var debate = session._debate;
|
|
4830
|
+
if (!debate || debate.phase !== "live") {
|
|
4831
|
+
sendTo(ws, { type: "debate_error", error: "No active debate." });
|
|
4832
|
+
return;
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
// If awaiting conclude confirmation, re-send the confirm prompt instead
|
|
4836
|
+
if (debate.awaitingConcludeConfirm) {
|
|
4837
|
+
sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
|
|
4838
|
+
return;
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
if (!msg.text) return;
|
|
4842
|
+
|
|
4843
|
+
debate.pendingComment = { text: msg.text };
|
|
4844
|
+
sendToSession(session.localId, { type: "debate_comment_queued", text: msg.text });
|
|
4845
|
+
|
|
4846
|
+
// If a panelist turn is in progress, abort it and go straight to moderator
|
|
4847
|
+
if (debate.turnInProgress && debate._currentTurnMateId && debate._currentTurnMateId !== debate.moderatorId) {
|
|
4848
|
+
var abortMateId = debate._currentTurnMateId;
|
|
4849
|
+
console.log("[debate] User raised hand during panelist turn, aborting " + abortMateId);
|
|
4850
|
+
|
|
4851
|
+
// Close the panelist's mention session to stop generation
|
|
4852
|
+
if (debate.panelistSessions[abortMateId]) {
|
|
4853
|
+
try { debate.panelistSessions[abortMateId].close(); } catch (e) {}
|
|
4854
|
+
delete debate.panelistSessions[abortMateId];
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
// Save partial text as interrupted turn
|
|
4858
|
+
var partialText = debate._currentTurnText || "(interrupted by audience)";
|
|
4859
|
+
var profile = getMateProfile(debate.mateCtx, abortMateId);
|
|
4860
|
+
var panelistInfo = null;
|
|
4861
|
+
for (var pi = 0; pi < debate.panelists.length; pi++) {
|
|
4862
|
+
if (debate.panelists[pi].mateId === abortMateId) { panelistInfo = debate.panelists[pi]; break; }
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
sendToSession(session.localId, {
|
|
4866
|
+
type: "debate_turn_done",
|
|
4867
|
+
mateId: abortMateId,
|
|
4868
|
+
mateName: profile.name,
|
|
4869
|
+
role: panelistInfo ? panelistInfo.role : "",
|
|
4870
|
+
text: partialText,
|
|
4871
|
+
interrupted: true,
|
|
4872
|
+
avatarStyle: profile.avatarStyle,
|
|
4873
|
+
avatarSeed: profile.avatarSeed,
|
|
4874
|
+
avatarColor: profile.avatarColor,
|
|
4875
|
+
});
|
|
4876
|
+
|
|
4877
|
+
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 };
|
|
4878
|
+
session.history.push(turnEntry);
|
|
4879
|
+
sm.appendToSessionFile(session, turnEntry);
|
|
4880
|
+
debate.history.push({ speaker: "panelist", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: partialText });
|
|
4881
|
+
|
|
4882
|
+
debate.turnInProgress = false;
|
|
4883
|
+
debate._currentTurnMateId = null;
|
|
4884
|
+
debate._currentTurnText = "";
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
// Inject to moderator immediately if no turn in progress (or just aborted)
|
|
4888
|
+
if (!debate.turnInProgress) {
|
|
4889
|
+
injectUserComment(session);
|
|
4890
|
+
}
|
|
4891
|
+
// If moderator is currently speaking, pendingComment will be picked up after moderator's onDone
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
function injectUserComment(session) {
|
|
4895
|
+
var debate = session._debate;
|
|
4896
|
+
if (!debate || !debate.pendingComment || !debate.moderatorSession || debate.phase === "ended") return;
|
|
4897
|
+
|
|
4898
|
+
var comment = debate.pendingComment;
|
|
4899
|
+
debate.pendingComment = null;
|
|
4900
|
+
|
|
4901
|
+
// Record in debate history
|
|
4902
|
+
debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: comment.text });
|
|
4903
|
+
|
|
4904
|
+
var commentEntry = { type: "debate_comment_injected", text: comment.text };
|
|
4905
|
+
session.history.push(commentEntry);
|
|
4906
|
+
sm.appendToSessionFile(session, commentEntry);
|
|
4907
|
+
sendToSession(session.localId, commentEntry);
|
|
4908
|
+
|
|
4909
|
+
// Feed to moderator
|
|
4910
|
+
debate.turnInProgress = true;
|
|
4911
|
+
var moderatorProfile = getMateProfile(debate.mateCtx, debate.moderatorId);
|
|
4912
|
+
|
|
4913
|
+
sendToSession(session.localId, {
|
|
4914
|
+
type: "debate_turn",
|
|
4915
|
+
mateId: debate.moderatorId,
|
|
4916
|
+
mateName: moderatorProfile.name,
|
|
4917
|
+
role: "moderator",
|
|
4918
|
+
round: debate.round,
|
|
4919
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
4920
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
4921
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
4922
|
+
});
|
|
4923
|
+
|
|
4924
|
+
var feedText = "[The user raised their hand and said:]\n" +
|
|
4925
|
+
comment.text + "\n" +
|
|
4926
|
+
"[Please acknowledge this and weave it into the discussion. Then continue the debate.]";
|
|
4927
|
+
|
|
4928
|
+
debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
function handleDebateStop(ws) {
|
|
4932
|
+
var session = getSessionForWs(ws);
|
|
4933
|
+
if (!session) return;
|
|
4934
|
+
|
|
4935
|
+
var debate = session._debate;
|
|
4936
|
+
if (!debate || debate.phase !== "live") return;
|
|
4937
|
+
|
|
4938
|
+
if (debate.turnInProgress) {
|
|
4939
|
+
// Let current turn finish, then end
|
|
4940
|
+
debate.phase = "ending";
|
|
4941
|
+
} else {
|
|
4942
|
+
endDebate(session, "user_stopped");
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
// Rebuild _debate from session history (for resume after server restart)
|
|
4947
|
+
function rebuildDebateState(session, ws) {
|
|
4948
|
+
// Find debate_started entry in history
|
|
4949
|
+
var startEntry = null;
|
|
4950
|
+
var endEntry = null;
|
|
4951
|
+
var concludeEntry = null;
|
|
4952
|
+
var lastRound = 1;
|
|
4953
|
+
for (var i = 0; i < session.history.length; i++) {
|
|
4954
|
+
var h = session.history[i];
|
|
4955
|
+
if (h.type === "debate_started") startEntry = h;
|
|
4956
|
+
if (h.type === "debate_ended") endEntry = h;
|
|
4957
|
+
if (h.type === "debate_conclude_confirm") concludeEntry = h;
|
|
4958
|
+
if (h.type === "debate_turn_done" && h.round) lastRound = h.round;
|
|
4959
|
+
}
|
|
4960
|
+
if (!startEntry) return null;
|
|
4961
|
+
|
|
4962
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
4963
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
4964
|
+
|
|
4965
|
+
var debate = {
|
|
4966
|
+
phase: endEntry ? "ended" : "live",
|
|
4967
|
+
topic: startEntry.topic || "",
|
|
4968
|
+
format: startEntry.format || "free_discussion",
|
|
4969
|
+
context: "",
|
|
4970
|
+
specialRequests: null,
|
|
4971
|
+
moderatorId: startEntry.moderatorId,
|
|
4972
|
+
panelists: (startEntry.panelists || []).map(function (p) {
|
|
4973
|
+
return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
|
|
4974
|
+
}),
|
|
4975
|
+
mateCtx: mateCtx,
|
|
4976
|
+
moderatorSession: null,
|
|
4977
|
+
panelistSessions: {},
|
|
4978
|
+
nameMap: buildDebateNameMap(
|
|
4979
|
+
(startEntry.panelists || []).map(function (p) { return { mateId: p.mateId, role: p.role || "" }; }),
|
|
4980
|
+
mateCtx
|
|
4981
|
+
),
|
|
4982
|
+
turnInProgress: false,
|
|
4983
|
+
pendingComment: null,
|
|
4984
|
+
round: lastRound,
|
|
4985
|
+
history: [],
|
|
4986
|
+
awaitingConcludeConfirm: !endEntry && !!concludeEntry,
|
|
4987
|
+
debateId: (session.loop && session.loop.loopId) || "debate_rebuilt",
|
|
4988
|
+
};
|
|
4989
|
+
|
|
4990
|
+
// Rebuild debate.history from session history turn entries
|
|
4991
|
+
for (var j = 0; j < session.history.length; j++) {
|
|
4992
|
+
var entry = session.history[j];
|
|
4993
|
+
if (entry.type === "debate_turn_done") {
|
|
4994
|
+
debate.history.push({
|
|
4995
|
+
speaker: entry.role === "moderator" ? "moderator" : "panelist",
|
|
4996
|
+
mateId: entry.mateId,
|
|
4997
|
+
mateName: entry.mateName,
|
|
4998
|
+
role: entry.role || "",
|
|
4999
|
+
text: entry.text || "",
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
|
|
5004
|
+
// If no endEntry and no concludeEntry, check if last moderator turn had no mentions (implicit conclude)
|
|
5005
|
+
if (!endEntry && !concludeEntry && debate.history.length > 0) {
|
|
5006
|
+
var lastTurn = debate.history[debate.history.length - 1];
|
|
5007
|
+
if (lastTurn.speaker === "moderator" && lastTurn.text) {
|
|
5008
|
+
var rebuildMentions = detectMentions(lastTurn.text, debate.nameMap);
|
|
5009
|
+
if (rebuildMentions.length === 0) {
|
|
5010
|
+
debate.awaitingConcludeConfirm = true;
|
|
5011
|
+
console.log("[debate] Last moderator turn had no mentions, setting awaitingConcludeConfirm.");
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
session._debate = debate;
|
|
5017
|
+
console.log("[debate] Rebuilt debate state from history. Topic:", debate.topic, "Phase:", debate.phase, "Turns:", debate.history.length);
|
|
5018
|
+
return debate;
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
function handleDebateConcludeResponse(ws, msg) {
|
|
5022
|
+
var session = getSessionForWs(ws);
|
|
5023
|
+
if (!session) return;
|
|
5024
|
+
var debate = session._debate;
|
|
5025
|
+
|
|
5026
|
+
// If _debate is gone (server restart), try to rebuild from history
|
|
5027
|
+
if (!debate) {
|
|
5028
|
+
debate = rebuildDebateState(session, ws);
|
|
5029
|
+
if (!debate) {
|
|
5030
|
+
console.log("[debate] Cannot rebuild debate state for resume.");
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
// Allow resume from both "live + awaiting confirm" and "ended" states
|
|
5036
|
+
var isLiveConfirm = debate.phase === "live" && debate.awaitingConcludeConfirm;
|
|
5037
|
+
var isResume = debate.phase === "ended" && msg.action === "continue";
|
|
5038
|
+
if (!isLiveConfirm && !isResume) return;
|
|
5039
|
+
|
|
5040
|
+
debate.awaitingConcludeConfirm = false;
|
|
5041
|
+
|
|
5042
|
+
if (msg.action === "end") {
|
|
5043
|
+
endDebate(session, "natural");
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
if (msg.action === "continue") {
|
|
5048
|
+
var wasEnded = debate.phase === "ended";
|
|
5049
|
+
debate.phase = "live";
|
|
5050
|
+
var instruction = (msg.text || "").trim();
|
|
5051
|
+
var mateCtx = debate.mateCtx || matesModule.buildMateCtx(ws._clayUser ? ws._clayUser.id : null);
|
|
5052
|
+
debate.mateCtx = mateCtx;
|
|
5053
|
+
var moderatorProfile = getMateProfile(mateCtx, debate.moderatorId);
|
|
5054
|
+
|
|
5055
|
+
// Record user's resume message if provided
|
|
5056
|
+
if (instruction) {
|
|
5057
|
+
var resumeEntry = { type: "debate_user_resume", text: instruction };
|
|
5058
|
+
session.history.push(resumeEntry);
|
|
5059
|
+
sm.appendToSessionFile(session, resumeEntry);
|
|
5060
|
+
sendToSession(session.localId, resumeEntry);
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
// Notify clients debate is back live and persist to history
|
|
5064
|
+
var resumedMsg = {
|
|
5065
|
+
type: "debate_resumed",
|
|
5066
|
+
topic: debate.topic,
|
|
5067
|
+
round: debate.round,
|
|
5068
|
+
moderatorId: debate.moderatorId,
|
|
5069
|
+
moderatorName: moderatorProfile.name,
|
|
5070
|
+
panelists: debate.panelists.map(function (p) {
|
|
5071
|
+
var prof = getMateProfile(mateCtx, p.mateId);
|
|
5072
|
+
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
5073
|
+
}),
|
|
5074
|
+
};
|
|
5075
|
+
session.history.push(resumedMsg);
|
|
5076
|
+
sm.appendToSessionFile(session, resumedMsg);
|
|
5077
|
+
sendToSession(session.localId, resumedMsg);
|
|
5078
|
+
|
|
5079
|
+
debate.turnInProgress = true;
|
|
5080
|
+
sendToSession(session.localId, {
|
|
5081
|
+
type: "debate_turn",
|
|
5082
|
+
mateId: debate.moderatorId,
|
|
5083
|
+
mateName: moderatorProfile.name,
|
|
5084
|
+
role: "moderator",
|
|
5085
|
+
round: debate.round,
|
|
5086
|
+
avatarColor: moderatorProfile.avatarColor,
|
|
5087
|
+
avatarStyle: moderatorProfile.avatarStyle,
|
|
5088
|
+
avatarSeed: moderatorProfile.avatarSeed,
|
|
5089
|
+
});
|
|
5090
|
+
|
|
5091
|
+
var resumePrompt = instruction
|
|
5092
|
+
? "[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.]"
|
|
5093
|
+
: "[The audience has requested the debate continue. Call on the next panelist with @TheirName to explore additional perspectives.]";
|
|
5094
|
+
|
|
5095
|
+
// If resuming from ended state, moderator session may be dead. Create a new one.
|
|
5096
|
+
if (wasEnded || !debate.moderatorSession || !debate.moderatorSession.isAlive()) {
|
|
5097
|
+
console.log("[debate] Creating new moderator session for resume");
|
|
5098
|
+
var claudeMd = loadMateClaudeMd(mateCtx, debate.moderatorId);
|
|
5099
|
+
var digests = loadMateDigests(mateCtx, debate.moderatorId);
|
|
5100
|
+
var moderatorContext = buildModeratorContext(debate) + digests;
|
|
5101
|
+
|
|
5102
|
+
// Include debate history so moderator has context
|
|
5103
|
+
moderatorContext += "\n\nDebate history so far:\n---\n";
|
|
5104
|
+
for (var hi = 0; hi < debate.history.length; hi++) {
|
|
5105
|
+
var h = debate.history[hi];
|
|
5106
|
+
moderatorContext += (h.mateName || h.speaker || "Unknown") + " (" + (h.role || "") + "): " + (h.text || "").slice(0, 500) + "\n\n";
|
|
5107
|
+
}
|
|
5108
|
+
moderatorContext += "---\n";
|
|
5109
|
+
|
|
5110
|
+
sdk.createMentionSession({
|
|
5111
|
+
claudeMd: claudeMd,
|
|
5112
|
+
initialContext: moderatorContext,
|
|
5113
|
+
initialMessage: resumePrompt,
|
|
5114
|
+
onActivity: function (activity) {
|
|
5115
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
5116
|
+
sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
|
|
5117
|
+
}
|
|
5118
|
+
},
|
|
5119
|
+
onDelta: function (delta) {
|
|
5120
|
+
if (session._debate && session._debate.phase !== "ended") {
|
|
5121
|
+
sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
|
|
5122
|
+
}
|
|
5123
|
+
},
|
|
5124
|
+
onDone: function (fullText) {
|
|
5125
|
+
handleModeratorTurnDone(session, fullText);
|
|
5126
|
+
},
|
|
5127
|
+
onError: function (errMsg) {
|
|
5128
|
+
console.error("[debate] Moderator resume error:", errMsg);
|
|
5129
|
+
endDebate(session, "error");
|
|
5130
|
+
},
|
|
5131
|
+
canUseTool: buildDebateToolHandler(session),
|
|
5132
|
+
}).then(function (mentionSession) {
|
|
5133
|
+
if (mentionSession) {
|
|
5134
|
+
debate.moderatorSession = mentionSession;
|
|
5135
|
+
}
|
|
5136
|
+
}).catch(function (err) {
|
|
5137
|
+
console.error("[debate] Failed to create resume moderator session:", err.message || err);
|
|
5138
|
+
endDebate(session, "error");
|
|
5139
|
+
});
|
|
5140
|
+
} else {
|
|
5141
|
+
debate.moderatorSession.pushMessage(resumePrompt, buildModeratorCallbacks(session));
|
|
5142
|
+
}
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5147
|
+
function endDebate(session, reason) {
|
|
5148
|
+
var debate = session._debate;
|
|
5149
|
+
if (!debate || debate.phase === "ended") return;
|
|
5150
|
+
|
|
5151
|
+
debate.phase = "ended";
|
|
5152
|
+
debate.turnInProgress = false;
|
|
5153
|
+
persistDebateState(session);
|
|
5154
|
+
|
|
5155
|
+
// Clean up brief watcher if still active
|
|
5156
|
+
if (debate._briefWatcher) {
|
|
5157
|
+
try { debate._briefWatcher.close(); } catch (e) {}
|
|
5158
|
+
debate._briefWatcher = null;
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
// Notify clients
|
|
5162
|
+
sendToSession(session.localId, {
|
|
5163
|
+
type: "debate_ended",
|
|
5164
|
+
reason: reason,
|
|
5165
|
+
rounds: debate.round,
|
|
5166
|
+
topic: debate.topic,
|
|
5167
|
+
});
|
|
5168
|
+
|
|
5169
|
+
// Save to session history
|
|
5170
|
+
var endEntry = { type: "debate_ended", topic: debate.topic, rounds: debate.round, reason: reason };
|
|
5171
|
+
session.history.push(endEntry);
|
|
5172
|
+
sm.appendToSessionFile(session, endEntry);
|
|
5173
|
+
|
|
5174
|
+
// Generate digests for all participants
|
|
5175
|
+
digestDebateParticipant(session, debate.moderatorId, debate, "moderator");
|
|
5176
|
+
for (var i = 0; i < debate.panelists.length; i++) {
|
|
5177
|
+
digestDebateParticipant(session, debate.panelists[i].mateId, debate, debate.panelists[i].role);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
function digestDebateParticipant(session, mateId, debate, role) {
|
|
5182
|
+
var mentionSession = null;
|
|
5183
|
+
if (mateId === debate.moderatorId) {
|
|
5184
|
+
mentionSession = debate.moderatorSession;
|
|
5185
|
+
} else {
|
|
5186
|
+
mentionSession = debate.panelistSessions[mateId];
|
|
5187
|
+
}
|
|
5188
|
+
if (!mentionSession || !mentionSession.isAlive()) return;
|
|
5189
|
+
|
|
5190
|
+
var mateDir = matesModule.getMateDir(debate.mateCtx, mateId);
|
|
5191
|
+
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
5192
|
+
|
|
5193
|
+
var digestPrompt = [
|
|
5194
|
+
"[SYSTEM: Debate Session Digest Request]",
|
|
5195
|
+
"The debate has ended. Summarize this debate from YOUR perspective for your long-term memory.",
|
|
5196
|
+
"Topic: " + debate.topic,
|
|
5197
|
+
"Your role: " + role,
|
|
5198
|
+
"Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
|
|
5199
|
+
"Schema:",
|
|
5200
|
+
"{",
|
|
5201
|
+
' "date": "YYYY-MM-DD",',
|
|
5202
|
+
' "type": "debate",',
|
|
5203
|
+
' "topic": "the debate topic",',
|
|
5204
|
+
' "my_role": "your role in the debate",',
|
|
5205
|
+
' "my_position": "what you argued/said",',
|
|
5206
|
+
' "other_perspectives": "key points from other participants",',
|
|
5207
|
+
' "outcome": "how the debate concluded",',
|
|
5208
|
+
' "open_items": "unresolved points"',
|
|
5209
|
+
"}",
|
|
5210
|
+
"",
|
|
5211
|
+
"IMPORTANT: Output ONLY the JSON object. Nothing else.",
|
|
5212
|
+
].join("\n");
|
|
5213
|
+
|
|
5214
|
+
var digestText = "";
|
|
5215
|
+
mentionSession.pushMessage(digestPrompt, {
|
|
5216
|
+
onActivity: function () {},
|
|
5217
|
+
onDelta: function (delta) {
|
|
5218
|
+
digestText += delta;
|
|
5219
|
+
},
|
|
5220
|
+
onDone: function () {
|
|
5221
|
+
var digestObj = null;
|
|
5222
|
+
try {
|
|
5223
|
+
var cleaned = digestText.trim();
|
|
5224
|
+
if (cleaned.indexOf("```") === 0) {
|
|
5225
|
+
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
5226
|
+
}
|
|
5227
|
+
digestObj = JSON.parse(cleaned);
|
|
5228
|
+
} catch (e) {
|
|
5229
|
+
console.error("[debate-digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
|
|
5230
|
+
digestObj = {
|
|
5231
|
+
date: new Date().toISOString().slice(0, 10),
|
|
5232
|
+
type: "debate",
|
|
5233
|
+
topic: debate.topic,
|
|
5234
|
+
my_role: role,
|
|
5235
|
+
raw: digestText.substring(0, 500),
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
try {
|
|
5240
|
+
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
5241
|
+
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
5242
|
+
fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
|
|
5243
|
+
} catch (e) {
|
|
5244
|
+
console.error("[debate-digest] Failed to write digest for mate " + mateId + ":", e.message);
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
// Close the session after digest
|
|
5248
|
+
mentionSession.close();
|
|
5249
|
+
},
|
|
5250
|
+
onError: function (err) {
|
|
5251
|
+
console.error("[debate-digest] Digest generation failed for mate " + mateId + ":", err);
|
|
5252
|
+
mentionSession.close();
|
|
5253
|
+
},
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
|
|
3881
5257
|
// --- Session presence (who is viewing which session) ---
|
|
3882
5258
|
function broadcastPresence() {
|
|
3883
5259
|
if (!usersModule.isMultiUser()) return;
|
|
@@ -3908,6 +5284,12 @@ function createProjectContext(opts) {
|
|
|
3908
5284
|
|
|
3909
5285
|
// --- WS disconnection handler ---
|
|
3910
5286
|
function handleDisconnection(ws) {
|
|
5287
|
+
// Persist last active session for this user before cleanup
|
|
5288
|
+
if (ws._clayActiveSession) {
|
|
5289
|
+
var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
5290
|
+
var dcExisting = userPresence.getPresence(slug, dcPresKey);
|
|
5291
|
+
userPresence.setPresence(slug, dcPresKey, ws._clayActiveSession, dcExisting ? dcExisting.mateDm : null);
|
|
5292
|
+
}
|
|
3911
5293
|
tm.detachAll(ws);
|
|
3912
5294
|
clients.delete(ws);
|
|
3913
5295
|
if (clients.size === 0) {
|
|
@@ -4126,15 +5508,38 @@ function createProjectContext(opts) {
|
|
|
4126
5508
|
var scopeFlag = scope === "global" ? "--global" : "--project";
|
|
4127
5509
|
var skillSpawnOpts = {
|
|
4128
5510
|
cwd: spawnCwd,
|
|
4129
|
-
stdio: "ignore",
|
|
5511
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4130
5512
|
detached: false,
|
|
4131
5513
|
};
|
|
4132
5514
|
if (skillUserInfo) {
|
|
4133
5515
|
skillSpawnOpts.uid = skillUserInfo.uid;
|
|
4134
5516
|
skillSpawnOpts.gid = skillUserInfo.gid;
|
|
4135
5517
|
}
|
|
5518
|
+
console.log("[skill-install] spawning: npx skills add " + url + " --skill " + skill + " --yes " + scopeFlag + " (cwd: " + spawnCwd + ")");
|
|
4136
5519
|
var child = spawn("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], skillSpawnOpts);
|
|
5520
|
+
var stdoutBuf = "";
|
|
5521
|
+
var stderrBuf = "";
|
|
5522
|
+
child.stdout.on("data", function (chunk) {
|
|
5523
|
+
stdoutBuf += chunk.toString();
|
|
5524
|
+
console.log("[skill-install] " + skill + " stdout chunk: " + chunk.toString().trim().slice(0, 500));
|
|
5525
|
+
});
|
|
5526
|
+
child.stderr.on("data", function (chunk) {
|
|
5527
|
+
stderrBuf += chunk.toString();
|
|
5528
|
+
console.log("[skill-install] " + skill + " stderr chunk: " + chunk.toString().trim().slice(0, 500));
|
|
5529
|
+
});
|
|
5530
|
+
// Timeout after 60 seconds
|
|
5531
|
+
var installTimeout = setTimeout(function () {
|
|
5532
|
+
console.error("[skill-install] " + skill + " timed out after 60s, killing process");
|
|
5533
|
+
try { child.kill("SIGTERM"); } catch (e) {}
|
|
5534
|
+
try {
|
|
5535
|
+
send({ type: "skill_installed", skill: skill, scope: scope, success: false, error: "Installation timed out after 60 seconds" });
|
|
5536
|
+
} catch (e) {}
|
|
5537
|
+
}, 60000);
|
|
4137
5538
|
child.on("close", function (code) {
|
|
5539
|
+
clearTimeout(installTimeout);
|
|
5540
|
+
console.log("[skill-install] " + skill + " exited with code " + code + " (stdout=" + stdoutBuf.length + "b, stderr=" + stderrBuf.length + "b)");
|
|
5541
|
+
if (stdoutBuf) console.log("[skill-install] stdout: " + stdoutBuf.slice(0, 2000));
|
|
5542
|
+
if (stderrBuf) console.log("[skill-install] stderr: " + stderrBuf.slice(0, 2000));
|
|
4138
5543
|
try {
|
|
4139
5544
|
var success = code === 0;
|
|
4140
5545
|
send({
|
|
@@ -4149,6 +5554,8 @@ function createProjectContext(opts) {
|
|
|
4149
5554
|
}
|
|
4150
5555
|
});
|
|
4151
5556
|
child.on("error", function (err) {
|
|
5557
|
+
clearTimeout(installTimeout);
|
|
5558
|
+
console.error("[skill-install] " + skill + " spawn error:", err.message || err);
|
|
4152
5559
|
try {
|
|
4153
5560
|
send({
|
|
4154
5561
|
type: "skill_installed",
|
|
@@ -4158,7 +5565,7 @@ function createProjectContext(opts) {
|
|
|
4158
5565
|
error: err.message,
|
|
4159
5566
|
});
|
|
4160
5567
|
} catch (e) {
|
|
4161
|
-
console.error("[
|
|
5568
|
+
console.error("[skill-install] " + skill + " send failed:", e.message || e);
|
|
4162
5569
|
}
|
|
4163
5570
|
});
|
|
4164
5571
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -4346,6 +5753,7 @@ function createProjectContext(opts) {
|
|
|
4346
5753
|
(function (skill) {
|
|
4347
5754
|
var installedVer = getInstalledVersion(skill.name);
|
|
4348
5755
|
var installed = !!installedVer;
|
|
5756
|
+
console.log("[skill-check] " + skill.name + " installed=" + installed + " localVersion=" + (installedVer || "none"));
|
|
4349
5757
|
// Convert GitHub repo URL to raw SKILL.md URL
|
|
4350
5758
|
var rawUrl = "";
|
|
4351
5759
|
var ghMatch = skill.url.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
@@ -4353,13 +5761,16 @@ function createProjectContext(opts) {
|
|
|
4353
5761
|
rawUrl = "https://raw.githubusercontent.com/" + ghMatch[1] + "/" + ghMatch[2] + "/main/SKILL.md";
|
|
4354
5762
|
}
|
|
4355
5763
|
if (!rawUrl) {
|
|
5764
|
+
console.log("[skill-check] " + skill.name + " no valid GitHub URL, skipping remote check");
|
|
4356
5765
|
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
|
|
4357
5766
|
finishOne();
|
|
4358
5767
|
return;
|
|
4359
5768
|
}
|
|
5769
|
+
console.log("[skill-check] " + skill.name + " fetching remote: " + rawUrl);
|
|
4360
5770
|
// Fetch remote SKILL.md
|
|
4361
5771
|
var https = require("https");
|
|
4362
5772
|
https.get(rawUrl, function (resp) {
|
|
5773
|
+
console.log("[skill-check] " + skill.name + " remote response status=" + resp.statusCode);
|
|
4363
5774
|
var data = "";
|
|
4364
5775
|
resp.on("data", function (chunk) { data += chunk; });
|
|
4365
5776
|
resp.on("end", function () {
|
|
@@ -4371,15 +5782,17 @@ function createProjectContext(opts) {
|
|
|
4371
5782
|
} else if (remoteVer && compareVersions(installedVer, remoteVer) < 0) {
|
|
4372
5783
|
status = "outdated";
|
|
4373
5784
|
}
|
|
5785
|
+
console.log("[skill-check] " + skill.name + " remoteVersion=" + remoteVer + " status=" + status);
|
|
4374
5786
|
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: remoteVer, status: status });
|
|
4375
5787
|
finishOne();
|
|
4376
5788
|
} catch (e) {
|
|
4377
|
-
console.error("[
|
|
5789
|
+
console.error("[skill-check] " + skill.name + " version parse failed:", e.message || e);
|
|
4378
5790
|
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "error" });
|
|
4379
5791
|
finishOne();
|
|
4380
5792
|
}
|
|
4381
5793
|
});
|
|
4382
|
-
}).on("error", function () {
|
|
5794
|
+
}).on("error", function (err) {
|
|
5795
|
+
console.error("[skill-check] " + skill.name + " fetch error:", err.message || err);
|
|
4383
5796
|
results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
|
|
4384
5797
|
finishOne();
|
|
4385
5798
|
});
|