chainlesschain 0.45.12 → 0.45.19

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 (78) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AppLayout-BfLjLMsm.js → AppLayout-B00RARl2.js} +1 -1
  3. package/src/assets/web-panel/assets/{Chat-DP7PO9Li.js → Chat-DXtvKoM0.js} +1 -1
  4. package/src/assets/web-panel/assets/{Cron-DyQF-7R1.js → Cron-BJ4ODHOy.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  6. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  7. package/src/assets/web-panel/assets/{Logs-BOii-AoO.js → Logs-CSeKZEG_.js} +1 -1
  8. package/src/assets/web-panel/assets/{McpTools-DmiJtJYr.js → McpTools-BYQAK11r.js} +1 -1
  9. package/src/assets/web-panel/assets/{Memory-CDRMMobU.js → Memory-gkUAPyuZ.js} +1 -1
  10. package/src/assets/web-panel/assets/{Notes-CVhqqoS1.js → Notes-bjNrQgAo.js} +1 -1
  11. package/src/assets/web-panel/assets/{Providers-Dkt7021l.js → Providers-Dbf57Tbv.js} +1 -1
  12. package/src/assets/web-panel/assets/{Services-DUDL_UGb.js → Services-CS0oMdxh.js} +1 -1
  13. package/src/assets/web-panel/assets/{Skills-DXXELJc3.js → Skills-B2fgruv8.js} +1 -1
  14. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  15. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  16. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  17. package/src/assets/web-panel/assets/{index-vW799KpE.js → index-CF2CqPYX.js} +2 -2
  18. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  19. package/src/assets/web-panel/index.html +1 -1
  20. package/src/commands/agent.js +7 -8
  21. package/src/commands/chat.js +9 -11
  22. package/src/commands/serve.js +11 -106
  23. package/src/commands/session.js +101 -0
  24. package/src/commands/ui.js +10 -151
  25. package/src/gateways/repl/agent-repl.js +1 -0
  26. package/src/gateways/repl/chat-repl.js +1 -0
  27. package/src/gateways/ui/web-ui-server.js +1 -0
  28. package/src/gateways/ws/action-protocol.js +83 -0
  29. package/src/gateways/ws/message-dispatcher.js +73 -0
  30. package/src/gateways/ws/session-protocol.js +396 -0
  31. package/src/gateways/ws/task-protocol.js +55 -0
  32. package/src/gateways/ws/worktree-protocol.js +315 -0
  33. package/src/gateways/ws/ws-server.js +4 -0
  34. package/src/gateways/ws/ws-session-gateway.js +1 -0
  35. package/src/harness/background-task-manager.js +506 -0
  36. package/src/harness/background-task-worker.js +48 -0
  37. package/src/harness/compression-telemetry.js +214 -0
  38. package/src/harness/feature-flags.js +157 -0
  39. package/src/harness/jsonl-session-store.js +452 -0
  40. package/src/harness/prompt-compressor.js +416 -0
  41. package/src/harness/worktree-isolator.js +845 -0
  42. package/src/lib/agent-core.js +246 -45
  43. package/src/lib/background-task-manager.js +1 -305
  44. package/src/lib/background-task-worker.js +1 -50
  45. package/src/lib/compression-telemetry.js +5 -0
  46. package/src/lib/feature-flags.js +7 -182
  47. package/src/lib/interaction-adapter.js +32 -6
  48. package/src/lib/jsonl-session-store.js +21 -237
  49. package/src/lib/prompt-compressor.js +10 -481
  50. package/src/lib/sub-agent-context.js +21 -1
  51. package/src/lib/worktree-isolator.js +13 -231
  52. package/src/lib/ws-agent-handler.js +1 -0
  53. package/src/lib/ws-server.js +138 -387
  54. package/src/lib/ws-session-manager.js +82 -1
  55. package/src/repl/agent-repl.js +11 -0
  56. package/src/runtime/agent-runtime.js +417 -0
  57. package/src/runtime/contracts/agent-turn.js +11 -0
  58. package/src/runtime/contracts/session-record.js +31 -0
  59. package/src/runtime/contracts/task-record.js +18 -0
  60. package/src/runtime/contracts/telemetry-record.js +23 -0
  61. package/src/runtime/contracts/worktree-record.js +14 -0
  62. package/src/runtime/index.js +13 -0
  63. package/src/runtime/policies/agent-policy.js +45 -0
  64. package/src/runtime/runtime-context.js +14 -0
  65. package/src/runtime/runtime-events.js +37 -0
  66. package/src/runtime/runtime-factory.js +50 -0
  67. package/src/tools/index.js +22 -0
  68. package/src/tools/legacy-agent-tools.js +171 -0
  69. package/src/tools/registry.js +141 -0
  70. package/src/tools/tool-context.js +28 -0
  71. package/src/tools/tool-permissions.js +28 -0
  72. package/src/tools/tool-telemetry.js +39 -0
  73. package/src/assets/web-panel/assets/Dashboard-BGGdnr6t.js +0 -3
  74. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  75. package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +0 -1
  76. package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +0 -1
  77. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  78. package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
@@ -11,6 +11,38 @@ import { spawn } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { dirname, join } from "node:path";
13
13
  import { WebSocketServer } from "ws";
14
+ import { createTaskRecord } from "../runtime/contracts/task-record.js";
15
+ import {
16
+ RUNTIME_EVENTS,
17
+ createRuntimeEvent,
18
+ } from "../runtime/runtime-events.js";
19
+ import { createWsMessageDispatcher } from "../gateways/ws/message-dispatcher.js";
20
+ import {
21
+ handleTaskDetail,
22
+ handleTaskHistory,
23
+ } from "../gateways/ws/task-protocol.js";
24
+ import {
25
+ handleSessionCreate,
26
+ handleSessionResume,
27
+ handleSessionMessage,
28
+ handleSessionPolicyUpdate,
29
+ handleSessionList,
30
+ handleSessionClose,
31
+ handleSessionAnswer,
32
+ handleHostToolResult,
33
+ } from "../gateways/ws/session-protocol.js";
34
+ import {
35
+ handleSlashCommand,
36
+ handleOrchestrate,
37
+ } from "../gateways/ws/action-protocol.js";
38
+ import {
39
+ handleWorktreeDiff,
40
+ handleWorktreeMerge,
41
+ handleWorktreeMergePreview,
42
+ handleWorktreeAutomationApply,
43
+ handleWorktreeList,
44
+ handleCompressionStats,
45
+ } from "../gateways/ws/worktree-protocol.js";
14
46
 
15
47
  const __filename = fileURLToPath(import.meta.url);
16
48
  const __dirname = dirname(__filename);
@@ -99,6 +131,7 @@ export class ChainlessChainWSServer extends EventEmitter {
99
131
 
100
132
  /** Session handlers: sessionId → WSAgentHandler | WSChatHandler */
101
133
  this.sessionHandlers = new Map();
134
+ this._dispatcher = createWsMessageDispatcher(this);
102
135
 
103
136
  this._heartbeatTimer = null;
104
137
  this._clientCounter = 0;
@@ -227,83 +260,7 @@ export class ChainlessChainWSServer extends EventEmitter {
227
260
 
228
261
  /** @private */
229
262
  async _handleMessage(clientId, ws, message) {
230
- const { id, type } = message;
231
-
232
- if (!id) {
233
- this._send(ws, {
234
- type: "error",
235
- code: "MISSING_ID",
236
- message: 'Message must include an "id" field',
237
- });
238
- return;
239
- }
240
-
241
- // Check authentication
242
- const client = this.clients.get(clientId);
243
- if (this.token && !client.authenticated && type !== "auth") {
244
- this._send(ws, {
245
- id,
246
- type: "error",
247
- code: "AUTH_REQUIRED",
248
- message: "Authentication required. Send an auth message first.",
249
- });
250
- return;
251
- }
252
-
253
- switch (type) {
254
- case "auth":
255
- this._handleAuth(clientId, ws, message);
256
- break;
257
- case "ping":
258
- this._send(ws, { id, type: "pong", serverTime: Date.now() });
259
- break;
260
- case "execute":
261
- this._executeCommand(id, ws, message.command, false);
262
- break;
263
- case "stream":
264
- this._executeCommand(id, ws, message.command, true);
265
- break;
266
- case "cancel":
267
- this._cancelRequest(id, ws);
268
- break;
269
- case "session-create":
270
- await this._handleSessionCreate(id, ws, message);
271
- break;
272
- case "session-resume":
273
- await this._handleSessionResume(id, ws, message);
274
- break;
275
- case "session-message":
276
- this._handleSessionMessage(id, ws, message);
277
- break;
278
- case "session-list":
279
- this._handleSessionList(id, ws);
280
- break;
281
- case "session-close":
282
- this._handleSessionClose(id, ws, message);
283
- break;
284
- case "slash-command":
285
- this._handleSlashCommand(id, ws, message);
286
- break;
287
- case "session-answer":
288
- this._handleSessionAnswer(id, ws, message);
289
- break;
290
- case "orchestrate":
291
- this._handleOrchestrate(id, ws, message);
292
- break;
293
- case "tasks-list":
294
- this._handleTasksList(id, ws);
295
- break;
296
- case "tasks-stop":
297
- this._handleTasksStop(id, ws, message);
298
- break;
299
- default:
300
- this._send(ws, {
301
- id,
302
- type: "error",
303
- code: "UNKNOWN_TYPE",
304
- message: `Unknown message type: ${type}`,
305
- });
306
- }
263
+ return this._dispatcher.dispatch(clientId, ws, message);
307
264
  }
308
265
 
309
266
  /**
@@ -320,84 +277,13 @@ export class ChainlessChainWSServer extends EventEmitter {
320
277
  * { type: "error", code: "ORCHESTRATE_FAILED", ... }
321
278
  */
322
279
  async _handleOrchestrate(id, ws, message) {
323
- const {
324
- task,
325
- cwd,
326
- agents = 3,
327
- ci = "npm test",
328
- noCi = false,
329
- strategy,
330
- } = message;
331
-
332
- if (!task || typeof task !== "string") {
333
- this._send(ws, {
334
- id,
335
- type: "error",
336
- code: "INVALID_TASK",
337
- message: "task field required",
338
- });
339
- return;
340
- }
341
-
342
- try {
343
- const { Orchestrator, TASK_SOURCE } = await import("./orchestrator.js");
344
-
345
- const orch = new Orchestrator({
346
- cwd: cwd || this.projectRoot || process.cwd(),
347
- maxParallel: Math.min(parseInt(agents, 10) || 3, 10),
348
- ciCommand: ci,
349
- agents: strategy ? { strategy } : undefined,
350
- verbose: false,
351
- });
352
-
353
- // Add WebSocket as a notification channel — all events go back to this client
354
- const wsNotifier = orch.notifier.addWebSocketChannel({
355
- send: (data) => this._send(ws, data),
356
- requestId: id,
357
- });
358
-
359
- // Forward real-time agent output
360
- orch.on("agent:output", (ev) => wsNotifier.sendAgentOutput(ev));
361
-
362
- // Forward task status changes
363
- orch.on("task:added", (t) => wsNotifier.sendStatus(t));
364
- orch.on("task:decomposing", (t) => wsNotifier.sendStatus(t));
365
- orch.on("ci:checking", ({ task: t }) => wsNotifier.sendStatus(t));
366
- orch.on("task:retrying", ({ task: t }) => wsNotifier.sendStatus(t));
367
-
368
- const result = await orch.addTask(task, {
369
- source: TASK_SOURCE.CLI,
370
- cwd: cwd || this.projectRoot || process.cwd(),
371
- runCI: !noCi,
372
- notify: true,
373
- });
374
-
375
- this._send(ws, {
376
- id,
377
- type: "orchestrate:done",
378
- taskId: result.id,
379
- status: result.status,
380
- retries: result.retries,
381
- subtasks: result.subtasks?.length || 0,
382
- });
383
- } catch (err) {
384
- this._send(ws, {
385
- id,
386
- type: "error",
387
- code: "ORCHESTRATE_FAILED",
388
- message: err.message,
389
- });
390
- }
280
+ return handleOrchestrate(this, id, ws, message);
391
281
  }
392
282
 
393
- /** @private list background tasks */
394
- _handleTasksList(id, ws) {
283
+ /** @private list background tasks */
284
+ async _handleTasksList(id, ws) {
395
285
  try {
396
- const { BackgroundTaskManager } = require("./background-task-manager.js");
397
- // Reuse singleton or create lightweight instance for listing
398
- if (!this._taskManager) {
399
- this._taskManager = new BackgroundTaskManager();
400
- }
286
+ await this._ensureTaskManager();
401
287
  const tasks = this._taskManager.list();
402
288
  this._send(ws, { id, type: "tasks-list", tasks });
403
289
  } catch (err) {
@@ -405,9 +291,35 @@ export class ChainlessChainWSServer extends EventEmitter {
405
291
  }
406
292
  }
407
293
 
408
- /** @private — stop a background task */
409
- _handleTasksStop(id, ws, message) {
294
+ /** @private — subscribe to task completion events and broadcast to all clients */
295
+ _subscribeTaskNotifications() {
296
+ if (!this._taskManager || this._taskNotificationsSubscribed) return;
297
+ this._taskNotificationsSubscribed = true;
298
+
299
+ this._taskManager.on("task:complete", (task) => {
300
+ const record = createTaskRecord(task, {
301
+ source: "background-task-manager",
302
+ });
303
+ this.emit(
304
+ RUNTIME_EVENTS.TASK_NOTIFICATION,
305
+ createRuntimeEvent(
306
+ RUNTIME_EVENTS.TASK_NOTIFICATION,
307
+ { task: record },
308
+ { kind: "server" },
309
+ ),
310
+ );
311
+ this._broadcast({
312
+ type: "task:notification",
313
+ task: record,
314
+ });
315
+ });
316
+ }
317
+
318
+ /** @private – stop a background task */
319
+ async _handleTasksStop(id, ws, message) {
410
320
  try {
321
+ await this._ensureTaskManager();
322
+
411
323
  if (this._taskManager && message.taskId) {
412
324
  this._taskManager.stop(message.taskId);
413
325
  this._send(ws, { id, type: "tasks-stopped", taskId: message.taskId });
@@ -429,6 +341,46 @@ export class ChainlessChainWSServer extends EventEmitter {
429
341
  }
430
342
  }
431
343
 
344
+ /** @private */
345
+ async _handleTaskDetail(id, ws, message) {
346
+ return handleTaskDetail(this, id, ws, message);
347
+ }
348
+
349
+ /** @private */
350
+ async _handleTaskHistory(id, ws, message) {
351
+ return handleTaskHistory(this, id, ws, message);
352
+ }
353
+
354
+ /** @private — diff preview for agent worktree branch */
355
+ async _handleWorktreeDiff(id, ws, message) {
356
+ return handleWorktreeDiff(this, id, ws, message);
357
+ }
358
+
359
+ /** @private — one-click merge of agent worktree branch */
360
+ async _handleWorktreeMerge(id, ws, message) {
361
+ return handleWorktreeMerge(this, id, ws, message);
362
+ }
363
+
364
+ /** @private - dry-run merge preview for an agent worktree branch */
365
+ async _handleWorktreeMergePreview(id, ws, message) {
366
+ return handleWorktreeMergePreview(this, id, ws, message);
367
+ }
368
+
369
+ /** @private - apply a safe automation candidate inside an agent worktree */
370
+ async _handleWorktreeAutomationApply(id, ws, message) {
371
+ return handleWorktreeAutomationApply(this, id, ws, message);
372
+ }
373
+
374
+ /** @private - list agent worktrees */
375
+ async _handleWorktreeList(id, ws) {
376
+ return handleWorktreeList(this, id, ws);
377
+ }
378
+
379
+ /** @private */
380
+ async _handleCompressionStats(id, ws, message) {
381
+ return handleCompressionStats(this, id, ws, message);
382
+ }
383
+
432
384
  /** @private */
433
385
  _handleAuth(clientId, ws, message) {
434
386
  const { id, token } = message;
@@ -619,268 +571,58 @@ export class ChainlessChainWSServer extends EventEmitter {
619
571
 
620
572
  /** @private */
621
573
  async _handleSessionCreate(id, ws, message) {
622
- if (!this.sessionManager) {
623
- this._send(ws, {
624
- id,
625
- type: "error",
626
- code: "NO_SESSION_SUPPORT",
627
- message: "Session support not configured on this server",
628
- });
629
- return;
630
- }
631
-
632
- const { sessionType, provider, model, apiKey, baseUrl, projectRoot } =
633
- message;
634
-
635
- try {
636
- const { sessionId } = this.sessionManager.createSession({
637
- type: sessionType || "agent",
638
- provider,
639
- model,
640
- apiKey,
641
- baseUrl,
642
- projectRoot,
643
- });
644
-
645
- const session = this.sessionManager.getSession(sessionId);
646
-
647
- // Lazy-load handler modules to avoid circular deps
648
- try {
649
- const { WebSocketInteractionAdapter } =
650
- await import("./interaction-adapter.js");
651
- session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
652
-
653
- let handler;
654
- if ((sessionType || "agent") === "chat") {
655
- const { WSChatHandler } = await import("./ws-chat-handler.js");
656
- handler = new WSChatHandler({
657
- session,
658
- interaction: session.interaction,
659
- });
660
- } else {
661
- const { WSAgentHandler } = await import("./ws-agent-handler.js");
662
- handler = new WSAgentHandler({
663
- session,
664
- interaction: session.interaction,
665
- db: this.sessionManager.db,
666
- });
667
- }
668
- this.sessionHandlers.set(sessionId, handler);
669
- } catch (_err) {
670
- // Handler creation failed — session still created, handler can be set later
671
- }
672
-
673
- this.emit("session:create", { sessionId, type: sessionType || "agent" });
674
-
675
- this._send(ws, {
676
- id,
677
- type: "session-created",
678
- sessionId,
679
- sessionType: sessionType || "agent",
680
- });
681
- } catch (err) {
682
- this._send(ws, {
683
- id,
684
- type: "error",
685
- code: "SESSION_CREATE_FAILED",
686
- message: err.message,
687
- });
688
- }
574
+ return handleSessionCreate(this, id, ws, message);
689
575
  }
690
576
 
691
577
  /** @private */
692
578
  async _handleSessionResume(id, ws, message) {
693
- if (!this.sessionManager) {
694
- this._send(ws, {
695
- id,
696
- type: "error",
697
- code: "NO_SESSION_SUPPORT",
698
- message: "Session support not configured",
699
- });
700
- return;
701
- }
702
-
703
- const { sessionId } = message;
704
- const session = this.sessionManager.resumeSession(sessionId);
705
-
706
- if (!session) {
707
- this._send(ws, {
708
- id,
709
- type: "error",
710
- code: "SESSION_NOT_FOUND",
711
- message: `Session not found: ${sessionId}`,
712
- });
713
- return;
714
- }
715
-
716
- // Rebuild interaction adapter and handler for the resumed session
717
- if (!this.sessionHandlers.has(sessionId)) {
718
- try {
719
- const { WebSocketInteractionAdapter } =
720
- await import("./interaction-adapter.js");
721
- session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
722
-
723
- let handler;
724
- if (session.type === "chat") {
725
- const { WSChatHandler } = await import("./ws-chat-handler.js");
726
- handler = new WSChatHandler({
727
- session,
728
- interaction: session.interaction,
729
- });
730
- } else {
731
- const { WSAgentHandler } = await import("./ws-agent-handler.js");
732
- handler = new WSAgentHandler({
733
- session,
734
- interaction: session.interaction,
735
- db: this.sessionManager.db,
736
- });
737
- }
738
- this.sessionHandlers.set(sessionId, handler);
739
- } catch (_err) {
740
- // Handler creation failed — session resumed without handler
741
- }
742
- }
743
-
744
- // Filter out system messages for history
745
- const history = (session.messages || []).filter((m) => m.role !== "system");
746
-
747
- this._send(ws, {
748
- id,
749
- type: "session-resumed",
750
- sessionId: session.id,
751
- history,
752
- });
579
+ return handleSessionResume(this, id, ws, message);
753
580
  }
754
581
 
755
582
  /** @private */
756
583
  _handleSessionMessage(id, ws, message) {
757
- const { sessionId, content } = message;
758
- const handler = this.sessionHandlers.get(sessionId);
759
-
760
- if (!handler) {
761
- this._send(ws, {
762
- id,
763
- type: "error",
764
- code: "SESSION_NOT_FOUND",
765
- message: `No active session handler for: ${sessionId}`,
766
- });
767
- return;
768
- }
584
+ return handleSessionMessage(this, id, ws, message);
585
+ }
769
586
 
770
- // Fire and forget — handler emits events via interaction adapter
771
- handler
772
- .handleMessage(content, id)
773
- .then(() => {
774
- // Persist messages after each turn
775
- if (this.sessionManager) {
776
- try {
777
- this.sessionManager.persistMessages(sessionId);
778
- } catch (_err) {
779
- // Non-critical
780
- }
781
- }
782
- })
783
- .catch((err) => {
784
- this._send(ws, {
785
- id,
786
- type: "error",
787
- code: "MESSAGE_FAILED",
788
- message: err.message,
789
- });
790
- });
587
+ /** @private */
588
+ _handleSessionPolicyUpdate(id, ws, message) {
589
+ return handleSessionPolicyUpdate(this, id, ws, message);
791
590
  }
792
591
 
793
592
  /** @private */
794
593
  _handleSessionList(id, ws) {
795
- if (!this.sessionManager) {
796
- this._send(ws, {
797
- id,
798
- type: "error",
799
- code: "NO_SESSION_SUPPORT",
800
- message: "Session support not configured",
801
- });
802
- return;
803
- }
804
-
805
- const sessions = this.sessionManager.listSessions();
806
- this._send(ws, {
807
- id,
808
- type: "session-list-result",
809
- sessions,
810
- });
594
+ return handleSessionList(this, id, ws);
811
595
  }
812
596
 
813
597
  /** @private */
814
598
  _handleSessionClose(id, ws, message) {
815
- const { sessionId } = message;
816
-
817
- // Remove handler
818
- const handler = this.sessionHandlers.get(sessionId);
819
- if (handler && handler.destroy) {
820
- handler.destroy();
821
- }
822
- this.sessionHandlers.delete(sessionId);
823
-
824
- // Close session in manager
825
- if (this.sessionManager) {
826
- try {
827
- this.sessionManager.closeSession(sessionId);
828
- } catch (_err) {
829
- // Non-critical
830
- }
831
- }
832
-
833
- this.emit("session:close", { sessionId });
834
-
835
- this._send(ws, {
836
- id,
837
- type: "result",
838
- success: true,
839
- sessionId,
840
- });
599
+ return handleSessionClose(this, id, ws, message);
841
600
  }
842
601
 
843
602
  /** @private */
844
603
  _handleSlashCommand(id, ws, message) {
845
- const { sessionId, command } = message;
846
- const handler = this.sessionHandlers.get(sessionId);
847
-
848
- if (!handler) {
849
- this._send(ws, {
850
- id,
851
- type: "error",
852
- code: "SESSION_NOT_FOUND",
853
- message: `No active session handler for: ${sessionId}`,
854
- });
855
- return;
856
- }
857
-
858
- handler.handleSlashCommand(command, id);
604
+ return handleSlashCommand(this, id, ws, message);
859
605
  }
860
606
 
861
607
  /** @private */
862
608
  _handleSessionAnswer(id, ws, message) {
863
- const { sessionId, requestId, answer } = message;
864
-
865
- if (!this.sessionManager) {
866
- this._send(ws, {
867
- id,
868
- type: "error",
869
- code: "NO_SESSION_SUPPORT",
870
- message: "Session support not configured",
871
- });
872
- return;
873
- }
874
-
875
- const session = this.sessionManager.getSession(sessionId);
876
- if (session && session.interaction && session.interaction.resolveAnswer) {
877
- session.interaction.resolveAnswer(requestId, answer);
878
- }
609
+ return handleSessionAnswer(this, id, ws, message);
610
+ }
879
611
 
880
- this._send(ws, { id, type: "result", success: true });
612
+ _handleHostToolResult(id, ws, message) {
613
+ return handleHostToolResult(this, id, ws, message);
881
614
  }
882
615
 
883
616
  /** @private — ping/pong heartbeat to detect dead connections */
617
+ async _ensureTaskManager() {
618
+ if (this._taskManager) return this._taskManager;
619
+ const { BackgroundTaskManager } =
620
+ await import("./background-task-manager.js");
621
+ this._taskManager = new BackgroundTaskManager({ recoverOnStart: true });
622
+ this._subscribeTaskNotifications();
623
+ return this._taskManager;
624
+ }
625
+
884
626
  _startHeartbeat() {
885
627
  this._heartbeatTimer = setInterval(() => {
886
628
  for (const [clientId, client] of this.clients) {
@@ -910,4 +652,13 @@ export class ChainlessChainWSServer extends EventEmitter {
910
652
  }
911
653
  }
912
654
  }
655
+
656
+ /** @private — broadcast a message to all connected, authenticated clients */
657
+ _broadcast(data) {
658
+ for (const [, client] of this.clients) {
659
+ if (client.authenticated || !this.token) {
660
+ this._send(client.ws, data);
661
+ }
662
+ }
663
+ }
913
664
  }