clay-server 3.8.1 → 4.0.0-beta.2

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.
Files changed (189) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/builtin-mates.js +6 -6
  3. package/lib/claude-hook-installer.js +15 -0
  4. package/lib/clay-history-mcp-server.js +62 -297
  5. package/lib/daemon.js +1 -1
  6. package/lib/debate-model-selection.js +119 -0
  7. package/lib/home-debate-tool-policy.js +164 -0
  8. package/lib/home-response-notifications.js +26 -0
  9. package/lib/mate-creation-mcp-server.js +55 -0
  10. package/lib/mate-ready-creation.js +95 -0
  11. package/lib/mates.js +2 -0
  12. package/lib/notes.js +2 -1
  13. package/lib/project-ask-user.js +64 -0
  14. package/lib/project-capsule-catalog.js +101 -0
  15. package/lib/project-connection.js +4 -0
  16. package/lib/project-debate-proposal.js +133 -17
  17. package/lib/project-debate.js +214 -110
  18. package/lib/project-delegated-follow-up.js +84 -0
  19. package/lib/project-delegated-session.js +53 -0
  20. package/lib/project-filesystem.js +7 -9
  21. package/lib/project-knowledge.js +1 -0
  22. package/lib/project-mate-creation-proposal.js +120 -0
  23. package/lib/project-memory.js +14 -7
  24. package/lib/project-models.js +157 -32
  25. package/lib/project-session-notes.js +2 -2
  26. package/lib/project-session-pair.js +8 -8
  27. package/lib/project-sessions.js +66 -51
  28. package/lib/project-vendor-login.js +397 -0
  29. package/lib/project-worker-proposal.js +25 -25
  30. package/lib/project-workspace-query.js +144 -0
  31. package/lib/project.js +349 -102
  32. package/lib/public/app.js +85 -42
  33. package/lib/public/css/avatar-imprints.css +0 -1
  34. package/lib/public/css/base.css +4 -0
  35. package/lib/public/css/capsule-ui.css +389 -0
  36. package/lib/public/css/command-palette.css +306 -6
  37. package/lib/public/css/home-capsule-library.css +243 -0
  38. package/lib/public/css/home-capsule-source.css +192 -0
  39. package/lib/public/css/home-debate-live.css +146 -0
  40. package/lib/public/css/home-debate-planning.css +125 -0
  41. package/lib/public/css/home-debates-archive.css +281 -0
  42. package/lib/public/css/home-hub.css +896 -405
  43. package/lib/public/css/home-mate-creation.css +34 -0
  44. package/lib/public/css/home-mate-model-picker.css +131 -0
  45. package/lib/public/css/home-mate-settings.css +344 -0
  46. package/lib/public/css/home-session-actions.css +265 -0
  47. package/lib/public/css/home-sidebar.css +667 -0
  48. package/lib/public/css/icon-strip.css +1 -2
  49. package/lib/public/css/mates.css +7 -34
  50. package/lib/public/css/pane.css +1 -1
  51. package/lib/public/css/playbook.css +0 -80
  52. package/lib/public/css/sticky-notes.css +53 -117
  53. package/lib/public/css/tui-attention.css +0 -44
  54. package/lib/public/css/workspace-assignment.css +45 -0
  55. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  56. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  57. package/lib/public/index.html +93 -74
  58. package/lib/public/modules/app-connection.js +10 -1
  59. package/lib/public/modules/app-dm.js +43 -160
  60. package/lib/public/modules/app-home-hub.js +328 -585
  61. package/lib/public/modules/app-message-cards.js +242 -0
  62. package/lib/public/modules/app-message-router.js +84 -0
  63. package/lib/public/modules/app-messages.js +102 -37
  64. package/lib/public/modules/app-notifications.js +8 -119
  65. package/lib/public/modules/app-projects.js +9 -1
  66. package/lib/public/modules/app-rendering.js +21 -602
  67. package/lib/public/modules/avatar-imprint.js +11 -8
  68. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  69. package/lib/public/modules/chat-render-runtime.js +198 -0
  70. package/lib/public/modules/command-palette.js +225 -436
  71. package/lib/public/modules/dm-render.js +82 -0
  72. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  73. package/lib/public/modules/home-capsule-library.js +146 -0
  74. package/lib/public/modules/home-capsule-source.js +383 -0
  75. package/lib/public/modules/home-chat-identity.js +35 -0
  76. package/lib/public/modules/home-chat-scroll.js +77 -0
  77. package/lib/public/modules/home-chat-stream-state.js +34 -0
  78. package/lib/public/modules/home-composer-focus.js +25 -0
  79. package/lib/public/modules/home-conversations-sheet.js +179 -0
  80. package/lib/public/modules/home-debate-activity.js +48 -0
  81. package/lib/public/modules/home-debate-controls.js +295 -0
  82. package/lib/public/modules/home-debate-launch.js +41 -0
  83. package/lib/public/modules/home-debate-live.js +284 -0
  84. package/lib/public/modules/home-debate-models.js +73 -0
  85. package/lib/public/modules/home-debate-planning.js +335 -0
  86. package/lib/public/modules/home-debates-archive.js +279 -0
  87. package/lib/public/modules/home-dock-resize.js +74 -0
  88. package/lib/public/modules/home-dock.js +446 -0
  89. package/lib/public/modules/home-mate-chat.js +496 -0
  90. package/lib/public/modules/home-mate-creation.js +109 -0
  91. package/lib/public/modules/home-mate-model-picker.js +281 -0
  92. package/lib/public/modules/home-mate-selection.js +38 -0
  93. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  94. package/lib/public/modules/home-mate-settings.js +383 -0
  95. package/lib/public/modules/home-session-actions.js +373 -0
  96. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  97. package/lib/public/modules/home-shell.js +9 -0
  98. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  99. package/lib/public/modules/home-sidebar.js +158 -0
  100. package/lib/public/modules/home-sub-surface.js +19 -0
  101. package/lib/public/modules/home-surface-boot.js +68 -0
  102. package/lib/public/modules/home-surface.js +141 -0
  103. package/lib/public/modules/home-tools.js +271 -0
  104. package/lib/public/modules/markdown.js +50 -2
  105. package/lib/public/modules/mate-management.js +86 -0
  106. package/lib/public/modules/mate-wizard.js +0 -7
  107. package/lib/public/modules/mention.js +17 -19
  108. package/lib/public/modules/pane-bridge.js +9 -0
  109. package/lib/public/modules/profile.js +4 -5
  110. package/lib/public/modules/project-settings.js +34 -28
  111. package/lib/public/modules/project-switcher.js +11 -5
  112. package/lib/public/modules/project-workspace-assignment.js +23 -0
  113. package/lib/public/modules/search-clay-chat.js +402 -0
  114. package/lib/public/modules/server-settings.js +14 -12
  115. package/lib/public/modules/session-actions.js +1 -1
  116. package/lib/public/modules/sidebar-mates.js +56 -552
  117. package/lib/public/modules/sidebar-mobile.js +7 -64
  118. package/lib/public/modules/sidebar-presence.js +37 -0
  119. package/lib/public/modules/split-pair-ui.js +13 -13
  120. package/lib/public/modules/split-view.js +7 -0
  121. package/lib/public/modules/sticky-notes.js +7 -6
  122. package/lib/public/modules/tool-input-composition.js +48 -0
  123. package/lib/public/modules/tool-llm-status.js +167 -0
  124. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  125. package/lib/public/modules/tool-renderer-chart.js +158 -0
  126. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  127. package/lib/public/modules/tool-renderer.js +464 -0
  128. package/lib/public/modules/tool-runtime.js +309 -0
  129. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  130. package/lib/public/modules/tool-ui-tree.js +21 -0
  131. package/lib/public/modules/tui-attention.js +10 -0
  132. package/lib/public/modules/user-settings.js +16 -23
  133. package/lib/public/modules/vendor-login.js +287 -0
  134. package/lib/public/modules/worker-proposal.js +2 -2
  135. package/lib/public/modules/workspace-assignment-card.js +149 -0
  136. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  137. package/lib/public/style.css +13 -2
  138. package/lib/runtime-env.js +65 -0
  139. package/lib/sdk-bridge.js +120 -32
  140. package/lib/sdk-message-processor.js +10 -6
  141. package/lib/server-home-capsule-creation.js +36 -0
  142. package/lib/server-home-chat-events.js +287 -0
  143. package/lib/server-home-chat.js +518 -0
  144. package/lib/server-home-clay-entry.js +65 -0
  145. package/lib/server-home-clay-session-links.js +26 -0
  146. package/lib/server-home-debate-planning.js +214 -0
  147. package/lib/server-home-debates.js +137 -0
  148. package/lib/server-home-mate-creation.js +261 -0
  149. package/lib/server-home-models.js +241 -0
  150. package/lib/server-home-preferences.js +61 -0
  151. package/lib/server-mates.js +37 -0
  152. package/lib/server-tools.js +377 -0
  153. package/lib/server.js +111 -59
  154. package/lib/session-pair-mcp-server.js +6 -6
  155. package/lib/sessions.js +18 -3
  156. package/lib/tool-capsule-source.js +142 -0
  157. package/lib/tool-control-mcp-server.js +126 -0
  158. package/lib/tool-llm.js +48 -0
  159. package/lib/tool-storage.js +77 -0
  160. package/lib/tool-ui-spec-advanced.js +82 -0
  161. package/lib/tool-ui-spec.js +261 -0
  162. package/lib/tools-registry.js +286 -0
  163. package/lib/users-home-dock-preferences.js +77 -0
  164. package/lib/users-home-surface-preferences.js +99 -0
  165. package/lib/users-preferences.js +4 -7
  166. package/lib/users.js +12 -0
  167. package/lib/whats-new-content.js +3 -3
  168. package/lib/workspace-assignment-service.js +420 -0
  169. package/lib/workspace-query-mcp-server.js +154 -0
  170. package/lib/workspace-query-service.js +492 -0
  171. package/lib/ws-schema.js +93 -12
  172. package/lib/yoke/adapters/acp.js +2 -1
  173. package/lib/yoke/adapters/antigravity.js +2 -1
  174. package/lib/yoke/adapters/claude-worker.js +11 -4
  175. package/lib/yoke/adapters/claude.js +73 -43
  176. package/lib/yoke/adapters/codex.js +40 -10
  177. package/lib/yoke/adapters/kiro.js +2 -1
  178. package/lib/yoke/claude-user-input.js +38 -0
  179. package/lib/yoke/codex-app-server.js +20 -2
  180. package/lib/yoke/codex-user-input.js +64 -0
  181. package/lib/yoke/complete-once.js +164 -0
  182. package/lib/yoke/index.js +5 -1
  183. package/lib/yoke/interface.js +19 -0
  184. package/lib/yoke/mcp-bridge-server.js +2 -6
  185. package/lib/yoke/user-input.js +254 -0
  186. package/package.json +3 -2
  187. package/lib/public/css/home-chat.css +0 -380
  188. package/lib/public/modules/home-chat.js +0 -438
  189. package/lib/server-clay-home.js +0 -245
@@ -2,6 +2,7 @@ var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var matesModule = require("./mates");
5
+ var debateToolPolicy = require("./home-debate-tool-policy");
5
6
 
6
7
  /**
7
8
  * Attach debate engine to a project context.
@@ -95,6 +96,7 @@ function attachDebate(ctx) {
95
96
  lines.push("4. After hearing from all panelists, you may start additional rounds.");
96
97
  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.");
97
98
  lines.push("6. If the user interjects with a comment, acknowledge it and weave it into the discussion.");
99
+ lines.push("7. You govern debate tool access: non-mutating investigation is permitted and shown to the user, while any capability that could change the system is blocked.");
98
100
  lines.push("");
99
101
  lines.push("Begin by introducing the topic and calling on the first panelist.");
100
102
  return lines.join("\n");
@@ -125,23 +127,43 @@ function attachDebate(ctx) {
125
127
  lines.push("2. Respond to the specific question or prompt from the moderator.");
126
128
  lines.push("3. You may reference what other panelists have said.");
127
129
  lines.push("4. Keep responses focused and substantive. Do not ramble.");
128
- lines.push("5. You have read-only access to project files if needed to support your arguments.");
130
+ lines.push("5. You may use investigation tools when they strengthen your argument. The moderator automatically permits non-mutating research and blocks system changes.");
129
131
  return lines.join("\n");
130
132
  }
131
133
 
132
- function buildDebateToolHandler(session) {
134
+ function buildDebateToolHandler(session, mateId) {
133
135
  return function (toolName, input, toolOpts) {
134
- var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
135
- if (autoAllow[toolName]) {
136
- return Promise.resolve({ behavior: "allow", updatedInput: input });
137
- }
136
+ var decision = debateToolPolicy.debateToolDecision(toolName, input);
137
+ var debate = session && session._debate;
138
+ var profile = debate ? ctx.getMateProfile(debate.mateCtx, mateId) : null;
139
+ debateSendAndRecord(session, {
140
+ type: "debate_tool_decision",
141
+ decisionId: toolOpts && toolOpts.toolUseID ? toolOpts.toolUseID : crypto.randomUUID(),
142
+ mateId: mateId || null,
143
+ mateName: profile && profile.name ? profile.name : "Mate",
144
+ toolName: debateToolPolicy.displayToolName(toolName),
145
+ action: decision.action,
146
+ command: decision.command,
147
+ commandTruncated: decision.commandTruncated,
148
+ decision: decision.allowed ? "allowed" : "blocked",
149
+ reason: decision.reason,
150
+ });
138
151
  return Promise.resolve({
139
- behavior: "deny",
140
- message: "Read-only access during debate. You cannot make changes.",
152
+ behavior: decision.behavior,
153
+ updatedInput: decision.updatedInput,
154
+ message: decision.message,
141
155
  });
142
156
  };
143
157
  }
144
158
 
159
+ function participantModel(debate, mateId) {
160
+ var selections = debate && Array.isArray(debate.participantModels) ? debate.participantModels : [];
161
+ for (var i = 0; i < selections.length; i++) {
162
+ if (selections[i] && selections[i].mateId === mateId) return selections[i];
163
+ }
164
+ return null;
165
+ }
166
+
145
167
  // --- State persistence ---
146
168
 
147
169
  function persistDebateState(session) {
@@ -154,6 +176,9 @@ function attachDebate(ctx) {
154
176
  context: d.context || "",
155
177
  specialRequests: d.specialRequests || null,
156
178
  moderatorId: d.moderatorId,
179
+ participantModels: Array.isArray(d.participantModels) ? d.participantModels.map(function (item) {
180
+ return { mateId: item.mateId, vendor: item.vendor, model: item.model };
181
+ }) : [],
157
182
  panelists: d.panelists.map(function (p) {
158
183
  return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
159
184
  }),
@@ -165,6 +190,7 @@ function attachDebate(ctx) {
165
190
  awaitingConcludeConfirm: !!d.awaitingConcludeConfirm,
166
191
  awaitingUserFloor: !!d.awaitingUserFloor,
167
192
  ownerId: d.ownerId || null,
193
+ turnSequence: d.turnSequence || 0,
168
194
  };
169
195
  ctx.sm.saveSessionFile(session);
170
196
  }
@@ -181,6 +207,7 @@ function attachDebate(ctx) {
181
207
  context: ds.context || "",
182
208
  specialRequests: ds.specialRequests || null,
183
209
  moderatorId: ds.moderatorId,
210
+ participantModels: Array.isArray(ds.participantModels) ? ds.participantModels : [],
184
211
  panelists: ds.panelists || [],
185
212
  mateCtx: mateCtx,
186
213
  moderatorSession: null,
@@ -197,6 +224,7 @@ function attachDebate(ctx) {
197
224
  awaitingConcludeConfirm: !!ds.awaitingConcludeConfirm,
198
225
  awaitingUserFloor: !!ds.awaitingUserFloor,
199
226
  ownerId: ds.ownerId || userId,
227
+ turnSequence: ds.turnSequence || 0,
200
228
  };
201
229
 
202
230
  // Fallback: if awaitingConcludeConfirm was not persisted, detect from history
@@ -337,11 +365,24 @@ function attachDebate(ctx) {
337
365
  // On server restart, SDK sessions are lost so debates cannot continue.
338
366
  // Clear stale debate state instead of restoring dead UI.
339
367
  ctx.sm.sessions.forEach(function (session) {
340
- if (session._debate) {
341
- delete session._debate;
342
- }
368
+ // A live in-memory debate belongs to this daemon and must survive a
369
+ // websocket reconnect. Persisted state without its runtime object is
370
+ // the restart case that cannot safely resume dead mention processes.
371
+ if (session._debate) return;
343
372
  if (session.debateState) {
344
373
  var phase = session.debateState.phase;
374
+ if (session.homeDebatePlanning === true && (phase === "live" || phase === "ending")) {
375
+ session.homeDebatePhase = "interrupted";
376
+ session.debateState.phase = "interrupted";
377
+ var lastLifecycle = null;
378
+ for (var li = session.history.length - 1; li >= 0; li--) {
379
+ var lifecycleType = session.history[li] && session.history[li].type;
380
+ if (lifecycleType === "debate_started" || lifecycleType === "debate_resumed" || lifecycleType === "debate_ended") { lastLifecycle = lifecycleType; break; }
381
+ }
382
+ if (lastLifecycle !== "debate_ended") session.history.push({ type: "debate_ended", topic: session.debateState.topic || session.title || "Debate", rounds: session.debateState.round || 1, reason: "interrupted", _ts: Date.now() });
383
+ ctx.sm.saveSessionFile(session);
384
+ return;
385
+ }
345
386
  if (phase === "preparing" || phase === "reviewing" || phase === "live") {
346
387
  console.log("[debate] Clearing stale debate state:", session.debateState.topic);
347
388
  session.debateState = null;
@@ -726,14 +767,31 @@ function attachDebate(ctx) {
726
767
 
727
768
  // Persist a debate message to session history and send to clients
728
769
  function debateSendAndRecord(session, msg) {
729
- session.history.push(msg);
730
- ctx.sm.appendToSessionFile(session, msg);
731
- ctx.sendToSession(session.localId, msg);
770
+ var debate = session._debate;
771
+ if (debate && msg.mateId && (msg.type === "debate_turn" || msg.type === "debate_turn_done")) {
772
+ var identity = ctx.getMateProfile(debate.mateCtx, msg.mateId);
773
+ var identityMate = matesModule.getMate(debate.mateCtx, msg.mateId);
774
+ var identityProfile = identityMate && identityMate.profile ? identityMate.profile : (identityMate || {});
775
+ if (!msg.mateName) msg.mateName = identity.name;
776
+ if (!msg.avatarStyle) msg.avatarStyle = identity.avatarStyle;
777
+ if (!msg.avatarSeed) msg.avatarSeed = identity.avatarSeed;
778
+ if (!msg.avatarColor) msg.avatarColor = identity.avatarColor;
779
+ if (!msg.avatarCustom && identityProfile.avatarCustom) msg.avatarCustom = identityProfile.avatarCustom;
780
+ }
781
+ if (debate && msg.type === "debate_turn") {
782
+ debate.turnSequence = (debate.turnSequence || 0) + 1;
783
+ debate.activeTurnId = (debate.debateId || "debate") + ":" + debate.turnSequence;
784
+ msg.turnId = debate.activeTurnId;
785
+ } else if (debate && !msg.turnId && (msg.type === "debate_activity" || msg.type === "debate_stream" || msg.type === "debate_turn_done")) {
786
+ msg.turnId = debate.activeTurnId || null;
787
+ }
788
+ ctx.sm.sendAndRecord(session, msg);
789
+ if (debate && msg.type === "debate_turn_done") debate.activeTurnId = null;
732
790
  }
733
791
 
734
792
  // --- Live debate ---
735
793
 
736
- function startDebateLive(session) {
794
+ function startDebateLive(session, targetWs) {
737
795
  var debate = session._debate;
738
796
  if (!debate || debate.phase === "live") return;
739
797
 
@@ -744,21 +802,35 @@ function attachDebate(ctx) {
744
802
  var mateCtx = debate.mateCtx;
745
803
  var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
746
804
 
747
- // Create a dedicated debate session, grouped with the setup session
748
- var liveOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
749
- var debateSession = ctx.sm.createSession(liveOpts);
750
- debateSession.title = debate.topic.slice(0, 50);
751
- debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
752
- ctx.sm.saveSessionFile(debateSession);
753
- ctx.sm.switchSession(debateSession.localId, null, ctx.hydrateImageRefs);
754
- debate.liveSessionId = debateSession.localId;
755
-
756
- // Move _debate to the new session so all debate logic uses it
757
- debateSession._debate = debate;
758
- delete session._debate;
759
- // Clear persisted state from setup session, persist on live session
760
- session.debateState = null;
761
- ctx.sm.saveSessionFile(session);
805
+ var reuseHomeSession = session.homeDebatePlanning === true;
806
+ var debateSession = session;
807
+ if (reuseHomeSession) {
808
+ session.homeDebatePhase = "live";
809
+ session.title = "Debate: " + debate.topic.slice(0, 42);
810
+ session.titleManuallySet = true;
811
+ session.isProcessing = false;
812
+ ctx.onProcessingChanged();
813
+ if (session.abortController) { try { session.abortController.abort(); } catch (e) {} }
814
+ if (session.queryInstance && typeof session.queryInstance.close === "function") { try { session.queryInstance.close(); } catch (e) {} }
815
+ session.abortController = null;
816
+ session.queryInstance = null;
817
+ if (session.messageQueue && typeof session.messageQueue.end === "function") { try { session.messageQueue.end(); } catch (e) {} }
818
+ session.messageQueue = null;
819
+ if (targetWs) targetWs._clayActiveSession = session.localId;
820
+ } else {
821
+ // Legacy project debates retain their dedicated live session.
822
+ var liveOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
823
+ debateSession = ctx.sm.createSession(liveOpts, targetWs || null);
824
+ debateSession.title = debate.topic.slice(0, 50);
825
+ debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
826
+ ctx.sm.saveSessionFile(debateSession);
827
+ ctx.sm.switchSession(debateSession.localId, null, ctx.hydrateImageRefs);
828
+ debate.liveSessionId = debateSession.localId;
829
+ debateSession._debate = debate;
830
+ delete session._debate;
831
+ session.debateState = null;
832
+ ctx.sm.saveSessionFile(session);
833
+ }
762
834
  persistDebateState(debateSession);
763
835
 
764
836
  // Save to session history
@@ -768,16 +840,15 @@ function attachDebate(ctx) {
768
840
  format: debate.format,
769
841
  moderatorId: debate.moderatorId,
770
842
  moderatorName: moderatorProfile.name,
843
+ participantModels: Array.isArray(debate.participantModels) ? debate.participantModels.map(function (item) {
844
+ return { mateId: item.mateId, vendor: item.vendor, model: item.model };
845
+ }) : [],
771
846
  panelists: debate.panelists.map(function (p) {
772
847
  var prof = ctx.getMateProfile(mateCtx, p.mateId);
773
848
  return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
774
849
  }),
775
850
  };
776
- debateSession.history.push(debateStartEntry);
777
- ctx.sm.appendToSessionFile(debateSession, debateStartEntry);
778
-
779
- // Notify clients (same data as history entry)
780
- ctx.sendToSession(debateSession.localId, debateStartEntry);
851
+ debateSendAndRecord(debateSession, debateStartEntry);
781
852
 
782
853
  // Signal moderator's first turn
783
854
  debateMateProcessing(debate.moderatorId, true);
@@ -798,15 +869,17 @@ function attachDebate(ctx) {
798
869
  var moderatorContext = buildModeratorContext(debate) + digests;
799
870
 
800
871
  var _modMate2 = matesModule.getMate(mateCtx, debate.moderatorId);
872
+ var moderatorModel = participantModel(debate, debate.moderatorId);
801
873
  ctx.sdk.createMentionSession({
802
- vendor: _modMate2 ? _modMate2.vendor : null,
874
+ vendor: moderatorModel ? moderatorModel.vendor : (_modMate2 ? _modMate2.vendor : null),
875
+ model: moderatorModel ? moderatorModel.model : undefined,
803
876
  linuxUser: mateCtx.linuxUser || undefined,
804
877
  claudeMd: claudeMd,
805
878
  initialContext: moderatorContext,
806
879
  initialMessage: "Begin the debate on: " + debate.topic,
807
880
  onActivity: function (activity) {
808
881
  if (debateSession._debate && debateSession._debate.phase !== "ended") {
809
- ctx.sendToSession(debateSession.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
882
+ debateSendAndRecord(debateSession, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
810
883
  }
811
884
  },
812
885
  onDelta: function (delta) {
@@ -821,7 +894,7 @@ function attachDebate(ctx) {
821
894
  console.error("[debate] Moderator error:", errMsg);
822
895
  endDebate(debateSession, "error");
823
896
  },
824
- canUseTool: buildDebateToolHandler(debateSession),
897
+ canUseTool: buildDebateToolHandler(debateSession, debate.moderatorId),
825
898
  }).then(function (mentionSession) {
826
899
  if (mentionSession) {
827
900
  debate.moderatorSession = mentionSession;
@@ -847,9 +920,7 @@ function attachDebate(ctx) {
847
920
 
848
921
  // Save to session history
849
922
  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 };
850
- session.history.push(turnEntry);
851
- ctx.sm.appendToSessionFile(session, turnEntry);
852
- ctx.sendToSession(session.localId, turnEntry);
923
+ debateSendAndRecord(session, turnEntry);
853
924
 
854
925
  // Check if user stopped the debate during this turn
855
926
  if (debate.phase === "ending") {
@@ -870,9 +941,7 @@ function attachDebate(ctx) {
870
941
  debate.awaitingConcludeConfirm = true;
871
942
  persistDebateState(session);
872
943
  var concludeEntry = { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round };
873
- session.history.push(concludeEntry);
874
- ctx.sm.appendToSessionFile(session, concludeEntry);
875
- ctx.sendToSession(session.localId, concludeEntry);
944
+ debateSendAndRecord(session, concludeEntry);
876
945
  return;
877
946
  }
878
947
 
@@ -933,7 +1002,7 @@ function attachDebate(ctx) {
933
1002
  var panelistCallbacks = {
934
1003
  onActivity: function (activity) {
935
1004
  if (session._debate && session._debate.phase !== "ended") {
936
- ctx.sendToSession(session.localId, { type: "debate_activity", mateId: mateId, activity: activity });
1005
+ debateSendAndRecord(session, { type: "debate_activity", mateId: mateId, activity: activity });
937
1006
  }
938
1007
  },
939
1008
  onDelta: function (delta) {
@@ -994,8 +1063,10 @@ function attachDebate(ctx) {
994
1063
  }
995
1064
 
996
1065
  var _panMate = matesModule.getMate(debate.mateCtx, mateId);
1066
+ var panelistModel = participantModel(debate, mateId);
997
1067
  ctx.sdk.createMentionSession({
998
- vendor: _panMate ? _panMate.vendor : null,
1068
+ vendor: panelistModel ? panelistModel.vendor : (_panMate ? _panMate.vendor : null),
1069
+ model: panelistModel ? panelistModel.model : undefined,
999
1070
  linuxUser: debate.mateCtx && debate.mateCtx.linuxUser ? debate.mateCtx.linuxUser : undefined,
1000
1071
  claudeMd: claudeMd,
1001
1072
  initialContext: panelistContext + historyContext,
@@ -1004,7 +1075,7 @@ function attachDebate(ctx) {
1004
1075
  onDelta: panelistCallbacks.onDelta,
1005
1076
  onDone: panelistCallbacks.onDone,
1006
1077
  onError: panelistCallbacks.onError,
1007
- canUseTool: buildDebateToolHandler(session),
1078
+ canUseTool: buildDebateToolHandler(session, mateId),
1008
1079
  }).then(function (mentionSession) {
1009
1080
  if (mentionSession) {
1010
1081
  debate.panelistSessions[mateId] = mentionSession;
@@ -1041,9 +1112,7 @@ function attachDebate(ctx) {
1041
1112
 
1042
1113
  // Save to session history
1043
1114
  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 };
1044
- session.history.push(turnEntry);
1045
- ctx.sm.appendToSessionFile(session, turnEntry);
1046
- ctx.sendToSession(session.localId, turnEntry);
1115
+ debateSendAndRecord(session, turnEntry);
1047
1116
 
1048
1117
  // Check if user stopped the debate
1049
1118
  if (debate.phase === "ending") {
@@ -1112,7 +1181,7 @@ function attachDebate(ctx) {
1112
1181
  return {
1113
1182
  onActivity: function (activity) {
1114
1183
  if (session._debate && session._debate.phase !== "ended") {
1115
- ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1184
+ debateSendAndRecord(session, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1116
1185
  }
1117
1186
  },
1118
1187
  onDelta: function (delta) {
@@ -1132,20 +1201,21 @@ function attachDebate(ctx) {
1132
1201
 
1133
1202
  // --- User interaction during debate ---
1134
1203
 
1135
- function handleDebateHandRaise(ws) {
1136
- var session = ctx.getSessionForWs(ws);
1204
+ function handleDebateHandRaise(ws, exactSession) {
1205
+ var session = exactSession || ctx.getSessionForWs(ws);
1137
1206
  if (!session) return;
1138
1207
 
1139
1208
  var debate = session._debate;
1140
1209
  if (!debate || debate.phase !== "live") return;
1141
1210
  if (debate.awaitingUserFloor || debate.handRaised) return;
1142
1211
  if (debate.awaitingConcludeConfirm) {
1143
- ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
1212
+ if (session.homeDebatePlanning === true) debateSendAndRecord(session, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
1213
+ else ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
1144
1214
  return;
1145
1215
  }
1146
1216
 
1147
1217
  debate.handRaised = true;
1148
- ctx.sendToSession(session.localId, { type: "debate_hand_raised" });
1218
+ debateSendAndRecord(session, { type: "debate_hand_raised" });
1149
1219
 
1150
1220
  // If no one is speaking, yield floor immediately
1151
1221
  if (!debate.turnInProgress) {
@@ -1154,9 +1224,9 @@ function attachDebate(ctx) {
1154
1224
  // Otherwise: current speaker finishes -> handRaised detected -> yieldFloorToUser
1155
1225
  }
1156
1226
 
1157
- function handleDebateComment(ws, msg) {
1227
+ function handleDebateComment(ws, msg, exactSession) {
1158
1228
  // Legacy: kept for compatibility but now hand raise is separate
1159
- var session = ctx.getSessionForWs(ws);
1229
+ var session = exactSession || ctx.getSessionForWs(ws);
1160
1230
  if (!session) return;
1161
1231
 
1162
1232
  var debate = session._debate;
@@ -1169,7 +1239,7 @@ function attachDebate(ctx) {
1169
1239
 
1170
1240
  debate.pendingComment = { text: msg.text };
1171
1241
  debate.handRaised = true;
1172
- ctx.sendToSession(session.localId, { type: "debate_comment_queued", text: msg.text });
1242
+ debateSendAndRecord(session, { type: "debate_comment_queued", text: msg.text });
1173
1243
 
1174
1244
  if (!debate.turnInProgress) {
1175
1245
  injectUserComment(session);
@@ -1215,9 +1285,7 @@ function attachDebate(ctx) {
1215
1285
  debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: comment.text });
1216
1286
 
1217
1287
  var commentEntry = { type: "debate_comment_injected", text: comment.text };
1218
- session.history.push(commentEntry);
1219
- ctx.sm.appendToSessionFile(session, commentEntry);
1220
- ctx.sendToSession(session.localId, commentEntry);
1288
+ debateSendAndRecord(session, commentEntry);
1221
1289
 
1222
1290
  // Feed to moderator: yield the floor to the user
1223
1291
  debate.turnInProgress = true;
@@ -1249,7 +1317,7 @@ function attachDebate(ctx) {
1249
1317
  return {
1250
1318
  onActivity: function (activity) {
1251
1319
  if (session._debate && session._debate.phase !== "ended") {
1252
- ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1320
+ debateSendAndRecord(session, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1253
1321
  }
1254
1322
  },
1255
1323
  onDelta: function (delta) {
@@ -1264,14 +1332,12 @@ function attachDebate(ctx) {
1264
1332
  // Record moderator yield turn
1265
1333
  debate.history.push({ speaker: "moderator", mateId: debate.moderatorId, mateName: moderatorProfile.name, text: fullText });
1266
1334
  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 };
1267
- session.history.push(turnEntry);
1268
- ctx.sm.appendToSessionFile(session, turnEntry);
1269
- ctx.sendToSession(session.localId, turnEntry);
1335
+ debateSendAndRecord(session, turnEntry);
1270
1336
 
1271
1337
  // Enter user floor mode: pause debate and show input
1272
1338
  debate.awaitingUserFloor = true;
1273
1339
  persistDebateState(session);
1274
- ctx.sendToSession(session.localId, { type: "debate_user_floor", topic: debate.topic, round: debate.round });
1340
+ debateSendAndRecord(session, { type: "debate_user_floor", topic: debate.topic, round: debate.round });
1275
1341
  },
1276
1342
  onError: function (errMsg) {
1277
1343
  console.error("[debate] Moderator yield error:", errMsg);
@@ -1280,8 +1346,8 @@ function attachDebate(ctx) {
1280
1346
  };
1281
1347
  }
1282
1348
 
1283
- function handleDebateUserFloorResponse(ws, msg) {
1284
- var session = ctx.getSessionForWs(ws);
1349
+ function handleDebateUserFloorResponse(ws, msg, exactSession) {
1350
+ var session = exactSession || ctx.getSessionForWs(ws);
1285
1351
  if (!session) return;
1286
1352
 
1287
1353
  var debate = session._debate;
@@ -1294,9 +1360,7 @@ function attachDebate(ctx) {
1294
1360
  // Record user's floor contribution
1295
1361
  debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: userText });
1296
1362
  var floorEntry = { type: "debate_user_floor_done", text: userText };
1297
- session.history.push(floorEntry);
1298
- ctx.sm.appendToSessionFile(session, floorEntry);
1299
- ctx.sendToSession(session.localId, floorEntry);
1363
+ debateSendAndRecord(session, floorEntry);
1300
1364
 
1301
1365
  // Feed to moderator to resume debate
1302
1366
  debate.turnInProgress = true;
@@ -1336,40 +1400,54 @@ function attachDebate(ctx) {
1336
1400
  startDebateLive(session);
1337
1401
  }
1338
1402
 
1339
- function handleDebateStop(ws) {
1340
- var session = ctx.getSessionForWs(ws);
1341
- if (!session) return;
1403
+ function handleDebateStop(ws, exactSession) {
1404
+ var session = exactSession || ctx.getSessionForWs(ws);
1405
+ if (!session) return false;
1342
1406
 
1343
1407
  var debate = session._debate;
1344
- if (!debate) return;
1408
+ if (!debate) return false;
1345
1409
 
1346
1410
  if (debate.phase === "reviewing") {
1347
1411
  endDebate(session, "user_stopped");
1348
- return;
1412
+ return true;
1349
1413
  }
1350
1414
 
1351
- if (debate.phase !== "live") return;
1415
+ if (debate.phase !== "live") return false;
1352
1416
 
1353
1417
  if (debate.turnInProgress) {
1354
1418
  // Let current turn finish, then end
1355
1419
  debate.phase = "ending";
1420
+ persistDebateState(session);
1421
+ debateSendAndRecord(session, { type: "debate_stop_requested", topic: debate.topic, round: debate.round });
1356
1422
  } else {
1357
1423
  endDebate(session, "user_stopped");
1358
1424
  }
1425
+ return true;
1426
+ }
1427
+
1428
+ function handleDebateCancelStop(ws, exactSession) {
1429
+ var session = exactSession || ctx.getSessionForWs(ws);
1430
+ var debate = session && session._debate;
1431
+ if (!debate || debate.phase !== "ending") return false;
1432
+ debate.phase = "live";
1433
+ persistDebateState(session);
1434
+ debateSendAndRecord(session, { type: "debate_stop_cancelled", topic: debate.topic, round: debate.round });
1435
+ return true;
1359
1436
  }
1360
1437
 
1361
1438
  // Rebuild _debate from session history (for resume after server restart)
1362
1439
  function rebuildDebateState(session, ws) {
1363
1440
  // Find debate_started entry in history
1364
1441
  var startEntry = null;
1365
- var endEntry = null;
1442
+ var latestPhase = null;
1366
1443
  var concludeEntry = null;
1367
1444
  var lastRound = 1;
1368
1445
  for (var i = 0; i < session.history.length; i++) {
1369
1446
  var h = session.history[i];
1370
- if (h.type === "debate_started") startEntry = h;
1371
- if (h.type === "debate_ended") endEntry = h;
1372
- if (h.type === "debate_conclude_confirm") concludeEntry = h;
1447
+ if (h.type === "debate_started") { startEntry = h; latestPhase = "live"; concludeEntry = null; }
1448
+ if (h.type === "debate_resumed") { latestPhase = "live"; concludeEntry = null; }
1449
+ if (h.type === "debate_ended") { latestPhase = "ended"; concludeEntry = null; }
1450
+ if (h.type === "debate_conclude_confirm" && latestPhase === "live") concludeEntry = h;
1373
1451
  if (h.type === "debate_turn_done" && h.round) lastRound = h.round;
1374
1452
  }
1375
1453
  if (!startEntry) return null;
@@ -1377,29 +1455,33 @@ function attachDebate(ctx) {
1377
1455
  var userId = ws._clayUser ? ws._clayUser.id : null;
1378
1456
  var mateCtx = matesModule.buildMateCtx(userId);
1379
1457
 
1458
+ var savedState = session.debateState || {};
1459
+ var savedPanelists = Array.isArray(savedState.panelists) && savedState.panelists.length ? savedState.panelists : (startEntry.panelists || []);
1380
1460
  var debate = {
1381
- phase: endEntry ? "ended" : "live",
1382
- topic: startEntry.topic || "",
1383
- format: startEntry.format || "free_discussion",
1384
- context: "",
1385
- specialRequests: null,
1386
- moderatorId: startEntry.moderatorId,
1387
- panelists: (startEntry.panelists || []).map(function (p) {
1461
+ phase: session.homeDebatePhase === "ended" || session.homeDebatePhase === "interrupted" ? "ended" : (latestPhase || "live"),
1462
+ topic: savedState.topic || startEntry.topic || "",
1463
+ format: savedState.format || startEntry.format || "free_discussion",
1464
+ context: savedState.context || "",
1465
+ specialRequests: savedState.specialRequests || null,
1466
+ moderatorId: savedState.moderatorId || startEntry.moderatorId,
1467
+ participantModels: Array.isArray(savedState.participantModels) ? savedState.participantModels : (startEntry.participantModels || []),
1468
+ panelists: savedPanelists.map(function (p) {
1388
1469
  return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
1389
1470
  }),
1390
1471
  mateCtx: mateCtx,
1391
1472
  moderatorSession: null,
1392
1473
  panelistSessions: {},
1393
1474
  nameMap: buildDebateNameMap(
1394
- (startEntry.panelists || []).map(function (p) { return { mateId: p.mateId, role: p.role || "" }; }),
1475
+ savedPanelists.map(function (p) { return { mateId: p.mateId, role: p.role || "" }; }),
1395
1476
  mateCtx
1396
1477
  ),
1397
1478
  turnInProgress: false,
1398
1479
  pendingComment: null,
1399
1480
  round: lastRound,
1400
1481
  history: [],
1401
- awaitingConcludeConfirm: !endEntry && !!concludeEntry,
1402
- debateId: (session.loop && session.loop.loopId) || "debate_rebuilt",
1482
+ awaitingConcludeConfirm: latestPhase === "live" && !!concludeEntry,
1483
+ ownerId: savedState.ownerId || userId,
1484
+ debateId: savedState.debateId || (session.loop && session.loop.loopId) || "debate_rebuilt",
1403
1485
  };
1404
1486
 
1405
1487
  // Rebuild debate.history from session history turn entries
@@ -1417,7 +1499,7 @@ function attachDebate(ctx) {
1417
1499
  }
1418
1500
 
1419
1501
  // If no endEntry and no concludeEntry, check if last moderator turn had no mentions (implicit conclude)
1420
- if (!endEntry && !concludeEntry && debate.history.length > 0) {
1502
+ if (debate.phase === "live" && !concludeEntry && debate.history.length > 0) {
1421
1503
  var lastTurn = debate.history[debate.history.length - 1];
1422
1504
  if (lastTurn.speaker === "moderator" && lastTurn.text) {
1423
1505
  var rebuildMentions = detectMentions(lastTurn.text, debate.nameMap);
@@ -1433,8 +1515,8 @@ function attachDebate(ctx) {
1433
1515
  return debate;
1434
1516
  }
1435
1517
 
1436
- function handleDebateConcludeResponse(ws, msg) {
1437
- var session = ctx.getSessionForWs(ws);
1518
+ function handleDebateConcludeResponse(ws, msg, exactSession) {
1519
+ var session = exactSession || ctx.getSessionForWs(ws);
1438
1520
  if (!session) return;
1439
1521
  var debate = session._debate;
1440
1522
 
@@ -1449,7 +1531,7 @@ function attachDebate(ctx) {
1449
1531
 
1450
1532
  // Allow resume from both "live + awaiting confirm" and "ended" states
1451
1533
  var isLiveConfirm = debate.phase === "live" && debate.awaitingConcludeConfirm;
1452
- var isResume = debate.phase === "ended" && msg.action === "continue";
1534
+ var isResume = (debate.phase === "ended" || debate.phase === "interrupted") && msg.action === "continue";
1453
1535
  if (!isLiveConfirm && !isResume) return;
1454
1536
 
1455
1537
  debate.awaitingConcludeConfirm = false;
@@ -1462,6 +1544,7 @@ function attachDebate(ctx) {
1462
1544
  if (msg.action === "continue") {
1463
1545
  var wasEnded = debate.phase === "ended";
1464
1546
  debate.phase = "live";
1547
+ if (session.homeDebatePlanning === true) session.homeDebatePhase = "live";
1465
1548
  var instruction = (msg.text || "").trim();
1466
1549
  var mateCtx = debate.mateCtx || matesModule.buildMateCtx(ws._clayUser ? ws._clayUser.id : null);
1467
1550
  debate.mateCtx = mateCtx;
@@ -1470,9 +1553,7 @@ function attachDebate(ctx) {
1470
1553
  // Record user's resume message if provided
1471
1554
  if (instruction) {
1472
1555
  var resumeEntry = { type: "debate_user_resume", text: instruction };
1473
- session.history.push(resumeEntry);
1474
- ctx.sm.appendToSessionFile(session, resumeEntry);
1475
- ctx.sendToSession(session.localId, resumeEntry);
1556
+ debateSendAndRecord(session, resumeEntry);
1476
1557
  }
1477
1558
 
1478
1559
  // Notify clients debate is back live and persist to history
@@ -1487,9 +1568,8 @@ function attachDebate(ctx) {
1487
1568
  return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
1488
1569
  }),
1489
1570
  };
1490
- session.history.push(resumedMsg);
1491
- ctx.sm.appendToSessionFile(session, resumedMsg);
1492
- ctx.sendToSession(session.localId, resumedMsg);
1571
+ persistDebateState(session);
1572
+ debateSendAndRecord(session, resumedMsg);
1493
1573
 
1494
1574
  debate.turnInProgress = true;
1495
1575
  debateMateProcessing(debate.moderatorId, true);
@@ -1531,15 +1611,17 @@ function attachDebate(ctx) {
1531
1611
  moderatorContext += "---\n";
1532
1612
 
1533
1613
  var _modMate3 = matesModule.getMate(mateCtx, debate.moderatorId);
1614
+ var resumedModeratorModel = participantModel(debate, debate.moderatorId);
1534
1615
  ctx.sdk.createMentionSession({
1535
- vendor: _modMate3 ? _modMate3.vendor : null,
1616
+ vendor: resumedModeratorModel ? resumedModeratorModel.vendor : (_modMate3 ? _modMate3.vendor : null),
1617
+ model: resumedModeratorModel ? resumedModeratorModel.model : undefined,
1536
1618
  linuxUser: mateCtx.linuxUser || undefined,
1537
1619
  claudeMd: claudeMd,
1538
1620
  initialContext: moderatorContext,
1539
1621
  initialMessage: resumePrompt,
1540
1622
  onActivity: function (activity) {
1541
1623
  if (session._debate && session._debate.phase !== "ended") {
1542
- ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1624
+ debateSendAndRecord(session, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1543
1625
  }
1544
1626
  },
1545
1627
  onDelta: function (delta) {
@@ -1554,7 +1636,7 @@ function attachDebate(ctx) {
1554
1636
  console.error("[debate] Moderator resume error:", errMsg);
1555
1637
  endDebate(session, "error");
1556
1638
  },
1557
- canUseTool: buildDebateToolHandler(session),
1639
+ canUseTool: buildDebateToolHandler(session, debate.moderatorId),
1558
1640
  }).then(function (mentionSession) {
1559
1641
  if (mentionSession) {
1560
1642
  debate.moderatorSession = mentionSession;
@@ -1584,6 +1666,7 @@ function attachDebate(ctx) {
1584
1666
 
1585
1667
  debate.phase = "ended";
1586
1668
  debate.turnInProgress = false;
1669
+ if (session.homeDebatePlanning === true) session.homeDebatePhase = reason === "interrupted" ? "interrupted" : "ended";
1587
1670
  persistDebateState(session);
1588
1671
 
1589
1672
  // Clean up brief watcher if still active
@@ -1593,17 +1676,20 @@ function attachDebate(ctx) {
1593
1676
  }
1594
1677
 
1595
1678
  // Notify clients
1596
- ctx.sendToSession(session.localId, {
1679
+ debateSendAndRecord(session, {
1597
1680
  type: "debate_ended",
1598
1681
  reason: reason,
1599
1682
  rounds: debate.round,
1600
1683
  topic: debate.topic,
1601
1684
  });
1602
1685
 
1603
- // Save to session history
1604
- var endEntry = { type: "debate_ended", topic: debate.topic, rounds: debate.round, reason: reason };
1605
- session.history.push(endEntry);
1606
- ctx.sm.appendToSessionFile(session, endEntry);
1686
+ if (debate.homeClient && session.homeDebatePlanning !== true) {
1687
+ if (typeof ctx.unregisterHomeDebateClient === "function") ctx.unregisterHomeDebateClient(debate.homeClient);
1688
+ debate.homeClient._clayActiveSession = debate.homeClient._clayHomePreviousSessionId || null;
1689
+ debate.homeClient._clayHomePreviousSessionId = null;
1690
+ debate.homeClient._clayHomeDebateSlug = null;
1691
+ debate.homeClient = null;
1692
+ }
1607
1693
 
1608
1694
  // Generate digests for all participants
1609
1695
  digestDebateParticipant(session, debate.moderatorId, debate, "moderator");
@@ -1725,6 +1811,7 @@ function attachDebate(ctx) {
1725
1811
  context: briefData.context || "",
1726
1812
  specialRequests: briefData.specialRequests || null,
1727
1813
  moderatorId: mateId,
1814
+ participantModels: Array.isArray(briefData.participantModels) ? briefData.participantModels : [],
1728
1815
  panelists: (briefData.panelists || []).map(function (p) {
1729
1816
  return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
1730
1817
  }),
@@ -1745,10 +1832,26 @@ function attachDebate(ctx) {
1745
1832
  session._debate = debate;
1746
1833
 
1747
1834
  console.log("[debate] MCP debate approved. Topic:", debate.topic, "debateId:", debateId);
1748
- startDebateLive(session);
1835
+ var homeWs = ws && ws._homeChatTap && ws._homeChatTap.mateSlug === ctx.slug && ws._homeChatTap.sessionId === session.localId ? ws : null;
1836
+ startDebateLive(session, homeWs);
1749
1837
  return { ok: true };
1750
1838
  }
1751
1839
 
1840
+ function handleHomeControl(ws, msg, session) {
1841
+ if (!session || session.homeDebatePlanning !== true) return false;
1842
+ if (!session._debate && (msg.action === "conclude" || msg.action === "resume")) rebuildDebateState(session, ws);
1843
+ if (!session._debate) return false;
1844
+ if (msg.action === "hand_raise") handleDebateHandRaise(ws, session);
1845
+ else if (msg.action === "comment") handleDebateComment(ws, msg, session);
1846
+ else if (msg.action === "stop") return handleDebateStop(ws, session);
1847
+ else if (msg.action === "cancel_stop") return handleDebateCancelStop(ws, session);
1848
+ else if (msg.action === "conclude") handleDebateConcludeResponse(ws, { action: msg.response, text: msg.text || "" }, session);
1849
+ else if (msg.action === "resume") handleDebateConcludeResponse(ws, { action: "continue", text: msg.text || "" }, session);
1850
+ else if (msg.action === "user_floor") handleDebateUserFloorResponse(ws, msg, session);
1851
+ else return false;
1852
+ return true;
1853
+ }
1854
+
1752
1855
  // --- Public API ---
1753
1856
 
1754
1857
  return {
@@ -1762,6 +1865,7 @@ function attachDebate(ctx) {
1762
1865
  restoreDebateState: restoreDebateState,
1763
1866
  checkForDmDebateBrief: checkForDmDebateBrief,
1764
1867
  handleMcpDebateApproval: handleMcpDebateApproval,
1868
+ handleHomeControl: handleHomeControl,
1765
1869
  };
1766
1870
  }
1767
1871