chainlesschain 0.45.11 → 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 (81) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/AppLayout-B00RARl2.js +1 -0
  3. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
  4. package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DXtvKoM0.js} +1 -1
  5. package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
  6. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  7. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  8. package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
  9. package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
  10. package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
  12. package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
  14. package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
  15. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  16. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  17. package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
  18. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  19. package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
  20. package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
  21. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  22. package/src/assets/web-panel/index.html +2 -2
  23. package/src/commands/agent.js +7 -8
  24. package/src/commands/chat.js +9 -11
  25. package/src/commands/serve.js +11 -106
  26. package/src/commands/session.js +185 -18
  27. package/src/commands/ui.js +10 -151
  28. package/src/gateways/repl/agent-repl.js +1 -0
  29. package/src/gateways/repl/chat-repl.js +1 -0
  30. package/src/gateways/ui/web-ui-server.js +1 -0
  31. package/src/gateways/ws/action-protocol.js +83 -0
  32. package/src/gateways/ws/message-dispatcher.js +73 -0
  33. package/src/gateways/ws/session-protocol.js +396 -0
  34. package/src/gateways/ws/task-protocol.js +55 -0
  35. package/src/gateways/ws/worktree-protocol.js +315 -0
  36. package/src/gateways/ws/ws-server.js +4 -0
  37. package/src/gateways/ws/ws-session-gateway.js +1 -0
  38. package/src/harness/background-task-manager.js +506 -0
  39. package/src/harness/background-task-worker.js +48 -0
  40. package/src/harness/compression-telemetry.js +214 -0
  41. package/src/harness/feature-flags.js +157 -0
  42. package/src/harness/jsonl-session-store.js +452 -0
  43. package/src/harness/prompt-compressor.js +416 -0
  44. package/src/harness/worktree-isolator.js +845 -0
  45. package/src/lib/agent-core.js +246 -45
  46. package/src/lib/background-task-manager.js +1 -305
  47. package/src/lib/background-task-worker.js +1 -50
  48. package/src/lib/compression-telemetry.js +5 -0
  49. package/src/lib/feature-flags.js +7 -182
  50. package/src/lib/interaction-adapter.js +32 -6
  51. package/src/lib/jsonl-session-store.js +21 -237
  52. package/src/lib/prompt-compressor.js +10 -351
  53. package/src/lib/sub-agent-context.js +91 -0
  54. package/src/lib/worktree-isolator.js +13 -231
  55. package/src/lib/ws-agent-handler.js +1 -0
  56. package/src/lib/ws-server.js +155 -359
  57. package/src/lib/ws-session-manager.js +82 -1
  58. package/src/repl/agent-repl.js +114 -32
  59. package/src/runtime/agent-runtime.js +417 -0
  60. package/src/runtime/contracts/agent-turn.js +11 -0
  61. package/src/runtime/contracts/session-record.js +31 -0
  62. package/src/runtime/contracts/task-record.js +18 -0
  63. package/src/runtime/contracts/telemetry-record.js +23 -0
  64. package/src/runtime/contracts/worktree-record.js +14 -0
  65. package/src/runtime/index.js +13 -0
  66. package/src/runtime/policies/agent-policy.js +45 -0
  67. package/src/runtime/runtime-context.js +14 -0
  68. package/src/runtime/runtime-events.js +37 -0
  69. package/src/runtime/runtime-factory.js +50 -0
  70. package/src/tools/index.js +22 -0
  71. package/src/tools/legacy-agent-tools.js +171 -0
  72. package/src/tools/registry.js +141 -0
  73. package/src/tools/tool-context.js +28 -0
  74. package/src/tools/tool-permissions.js +28 -0
  75. package/src/tools/tool-telemetry.js +39 -0
  76. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
  77. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
  78. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  79. package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
  80. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  81. 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,77 +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
- default:
294
- this._send(ws, {
295
- id,
296
- type: "error",
297
- code: "UNKNOWN_TYPE",
298
- message: `Unknown message type: ${type}`,
299
- });
300
- }
263
+ return this._dispatcher.dispatch(clientId, ws, message);
301
264
  }
302
265
 
303
266
  /**
@@ -314,76 +277,110 @@ export class ChainlessChainWSServer extends EventEmitter {
314
277
  * { type: "error", code: "ORCHESTRATE_FAILED", ... }
315
278
  */
316
279
  async _handleOrchestrate(id, ws, message) {
317
- const {
318
- task,
319
- cwd,
320
- agents = 3,
321
- ci = "npm test",
322
- noCi = false,
323
- strategy,
324
- } = message;
325
-
326
- if (!task || typeof task !== "string") {
327
- this._send(ws, {
328
- id,
329
- type: "error",
330
- code: "INVALID_TASK",
331
- message: "task field required",
332
- });
333
- return;
334
- }
280
+ return handleOrchestrate(this, id, ws, message);
281
+ }
335
282
 
283
+ /** @private – list background tasks */
284
+ async _handleTasksList(id, ws) {
336
285
  try {
337
- const { Orchestrator, TASK_SOURCE } = await import("./orchestrator.js");
338
-
339
- const orch = new Orchestrator({
340
- cwd: cwd || this.projectRoot || process.cwd(),
341
- maxParallel: Math.min(parseInt(agents, 10) || 3, 10),
342
- ciCommand: ci,
343
- agents: strategy ? { strategy } : undefined,
344
- verbose: false,
345
- });
286
+ await this._ensureTaskManager();
287
+ const tasks = this._taskManager.list();
288
+ this._send(ws, { id, type: "tasks-list", tasks });
289
+ } catch (err) {
290
+ this._send(ws, { id, type: "tasks-list", tasks: [] });
291
+ }
292
+ }
346
293
 
347
- // Add WebSocket as a notification channel — all events go back to this client
348
- const wsNotifier = orch.notifier.addWebSocketChannel({
349
- send: (data) => this._send(ws, data),
350
- requestId: id,
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,
351
314
  });
315
+ });
316
+ }
352
317
 
353
- // Forward real-time agent output
354
- orch.on("agent:output", (ev) => wsNotifier.sendAgentOutput(ev));
355
-
356
- // Forward task status changes
357
- orch.on("task:added", (t) => wsNotifier.sendStatus(t));
358
- orch.on("task:decomposing", (t) => wsNotifier.sendStatus(t));
359
- orch.on("ci:checking", ({ task: t }) => wsNotifier.sendStatus(t));
360
- orch.on("task:retrying", ({ task: t }) => wsNotifier.sendStatus(t));
361
-
362
- const result = await orch.addTask(task, {
363
- source: TASK_SOURCE.CLI,
364
- cwd: cwd || this.projectRoot || process.cwd(),
365
- runCI: !noCi,
366
- notify: true,
367
- });
318
+ /** @private stop a background task */
319
+ async _handleTasksStop(id, ws, message) {
320
+ try {
321
+ await this._ensureTaskManager();
368
322
 
369
- this._send(ws, {
370
- id,
371
- type: "orchestrate:done",
372
- taskId: result.id,
373
- status: result.status,
374
- retries: result.retries,
375
- subtasks: result.subtasks?.length || 0,
376
- });
323
+ if (this._taskManager && message.taskId) {
324
+ this._taskManager.stop(message.taskId);
325
+ this._send(ws, { id, type: "tasks-stopped", taskId: message.taskId });
326
+ } else {
327
+ this._send(ws, {
328
+ id,
329
+ type: "error",
330
+ code: "NO_TASK",
331
+ message: "taskId required or no task manager",
332
+ });
333
+ }
377
334
  } catch (err) {
378
335
  this._send(ws, {
379
336
  id,
380
337
  type: "error",
381
- code: "ORCHESTRATE_FAILED",
338
+ code: "TASKS_STOP_FAILED",
382
339
  message: err.message,
383
340
  });
384
341
  }
385
342
  }
386
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
+
387
384
  /** @private */
388
385
  _handleAuth(clientId, ws, message) {
389
386
  const { id, token } = message;
@@ -574,268 +571,58 @@ export class ChainlessChainWSServer extends EventEmitter {
574
571
 
575
572
  /** @private */
576
573
  async _handleSessionCreate(id, ws, message) {
577
- if (!this.sessionManager) {
578
- this._send(ws, {
579
- id,
580
- type: "error",
581
- code: "NO_SESSION_SUPPORT",
582
- message: "Session support not configured on this server",
583
- });
584
- return;
585
- }
586
-
587
- const { sessionType, provider, model, apiKey, baseUrl, projectRoot } =
588
- message;
589
-
590
- try {
591
- const { sessionId } = this.sessionManager.createSession({
592
- type: sessionType || "agent",
593
- provider,
594
- model,
595
- apiKey,
596
- baseUrl,
597
- projectRoot,
598
- });
599
-
600
- const session = this.sessionManager.getSession(sessionId);
601
-
602
- // Lazy-load handler modules to avoid circular deps
603
- try {
604
- const { WebSocketInteractionAdapter } =
605
- await import("./interaction-adapter.js");
606
- session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
607
-
608
- let handler;
609
- if ((sessionType || "agent") === "chat") {
610
- const { WSChatHandler } = await import("./ws-chat-handler.js");
611
- handler = new WSChatHandler({
612
- session,
613
- interaction: session.interaction,
614
- });
615
- } else {
616
- const { WSAgentHandler } = await import("./ws-agent-handler.js");
617
- handler = new WSAgentHandler({
618
- session,
619
- interaction: session.interaction,
620
- db: this.sessionManager.db,
621
- });
622
- }
623
- this.sessionHandlers.set(sessionId, handler);
624
- } catch (_err) {
625
- // Handler creation failed — session still created, handler can be set later
626
- }
627
-
628
- this.emit("session:create", { sessionId, type: sessionType || "agent" });
629
-
630
- this._send(ws, {
631
- id,
632
- type: "session-created",
633
- sessionId,
634
- sessionType: sessionType || "agent",
635
- });
636
- } catch (err) {
637
- this._send(ws, {
638
- id,
639
- type: "error",
640
- code: "SESSION_CREATE_FAILED",
641
- message: err.message,
642
- });
643
- }
574
+ return handleSessionCreate(this, id, ws, message);
644
575
  }
645
576
 
646
577
  /** @private */
647
578
  async _handleSessionResume(id, ws, message) {
648
- if (!this.sessionManager) {
649
- this._send(ws, {
650
- id,
651
- type: "error",
652
- code: "NO_SESSION_SUPPORT",
653
- message: "Session support not configured",
654
- });
655
- return;
656
- }
657
-
658
- const { sessionId } = message;
659
- const session = this.sessionManager.resumeSession(sessionId);
660
-
661
- if (!session) {
662
- this._send(ws, {
663
- id,
664
- type: "error",
665
- code: "SESSION_NOT_FOUND",
666
- message: `Session not found: ${sessionId}`,
667
- });
668
- return;
669
- }
670
-
671
- // Rebuild interaction adapter and handler for the resumed session
672
- if (!this.sessionHandlers.has(sessionId)) {
673
- try {
674
- const { WebSocketInteractionAdapter } =
675
- await import("./interaction-adapter.js");
676
- session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
677
-
678
- let handler;
679
- if (session.type === "chat") {
680
- const { WSChatHandler } = await import("./ws-chat-handler.js");
681
- handler = new WSChatHandler({
682
- session,
683
- interaction: session.interaction,
684
- });
685
- } else {
686
- const { WSAgentHandler } = await import("./ws-agent-handler.js");
687
- handler = new WSAgentHandler({
688
- session,
689
- interaction: session.interaction,
690
- db: this.sessionManager.db,
691
- });
692
- }
693
- this.sessionHandlers.set(sessionId, handler);
694
- } catch (_err) {
695
- // Handler creation failed — session resumed without handler
696
- }
697
- }
698
-
699
- // Filter out system messages for history
700
- const history = (session.messages || []).filter((m) => m.role !== "system");
701
-
702
- this._send(ws, {
703
- id,
704
- type: "session-resumed",
705
- sessionId: session.id,
706
- history,
707
- });
579
+ return handleSessionResume(this, id, ws, message);
708
580
  }
709
581
 
710
582
  /** @private */
711
583
  _handleSessionMessage(id, ws, message) {
712
- const { sessionId, content } = message;
713
- const handler = this.sessionHandlers.get(sessionId);
714
-
715
- if (!handler) {
716
- this._send(ws, {
717
- id,
718
- type: "error",
719
- code: "SESSION_NOT_FOUND",
720
- message: `No active session handler for: ${sessionId}`,
721
- });
722
- return;
723
- }
584
+ return handleSessionMessage(this, id, ws, message);
585
+ }
724
586
 
725
- // Fire and forget — handler emits events via interaction adapter
726
- handler
727
- .handleMessage(content, id)
728
- .then(() => {
729
- // Persist messages after each turn
730
- if (this.sessionManager) {
731
- try {
732
- this.sessionManager.persistMessages(sessionId);
733
- } catch (_err) {
734
- // Non-critical
735
- }
736
- }
737
- })
738
- .catch((err) => {
739
- this._send(ws, {
740
- id,
741
- type: "error",
742
- code: "MESSAGE_FAILED",
743
- message: err.message,
744
- });
745
- });
587
+ /** @private */
588
+ _handleSessionPolicyUpdate(id, ws, message) {
589
+ return handleSessionPolicyUpdate(this, id, ws, message);
746
590
  }
747
591
 
748
592
  /** @private */
749
593
  _handleSessionList(id, ws) {
750
- if (!this.sessionManager) {
751
- this._send(ws, {
752
- id,
753
- type: "error",
754
- code: "NO_SESSION_SUPPORT",
755
- message: "Session support not configured",
756
- });
757
- return;
758
- }
759
-
760
- const sessions = this.sessionManager.listSessions();
761
- this._send(ws, {
762
- id,
763
- type: "session-list-result",
764
- sessions,
765
- });
594
+ return handleSessionList(this, id, ws);
766
595
  }
767
596
 
768
597
  /** @private */
769
598
  _handleSessionClose(id, ws, message) {
770
- const { sessionId } = message;
771
-
772
- // Remove handler
773
- const handler = this.sessionHandlers.get(sessionId);
774
- if (handler && handler.destroy) {
775
- handler.destroy();
776
- }
777
- this.sessionHandlers.delete(sessionId);
778
-
779
- // Close session in manager
780
- if (this.sessionManager) {
781
- try {
782
- this.sessionManager.closeSession(sessionId);
783
- } catch (_err) {
784
- // Non-critical
785
- }
786
- }
787
-
788
- this.emit("session:close", { sessionId });
789
-
790
- this._send(ws, {
791
- id,
792
- type: "result",
793
- success: true,
794
- sessionId,
795
- });
599
+ return handleSessionClose(this, id, ws, message);
796
600
  }
797
601
 
798
602
  /** @private */
799
603
  _handleSlashCommand(id, ws, message) {
800
- const { sessionId, command } = message;
801
- const handler = this.sessionHandlers.get(sessionId);
802
-
803
- if (!handler) {
804
- this._send(ws, {
805
- id,
806
- type: "error",
807
- code: "SESSION_NOT_FOUND",
808
- message: `No active session handler for: ${sessionId}`,
809
- });
810
- return;
811
- }
812
-
813
- handler.handleSlashCommand(command, id);
604
+ return handleSlashCommand(this, id, ws, message);
814
605
  }
815
606
 
816
607
  /** @private */
817
608
  _handleSessionAnswer(id, ws, message) {
818
- const { sessionId, requestId, answer } = message;
819
-
820
- if (!this.sessionManager) {
821
- this._send(ws, {
822
- id,
823
- type: "error",
824
- code: "NO_SESSION_SUPPORT",
825
- message: "Session support not configured",
826
- });
827
- return;
828
- }
829
-
830
- const session = this.sessionManager.getSession(sessionId);
831
- if (session && session.interaction && session.interaction.resolveAnswer) {
832
- session.interaction.resolveAnswer(requestId, answer);
833
- }
609
+ return handleSessionAnswer(this, id, ws, message);
610
+ }
834
611
 
835
- this._send(ws, { id, type: "result", success: true });
612
+ _handleHostToolResult(id, ws, message) {
613
+ return handleHostToolResult(this, id, ws, message);
836
614
  }
837
615
 
838
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
+
839
626
  _startHeartbeat() {
840
627
  this._heartbeatTimer = setInterval(() => {
841
628
  for (const [clientId, client] of this.clients) {
@@ -865,4 +652,13 @@ export class ChainlessChainWSServer extends EventEmitter {
865
652
  }
866
653
  }
867
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
+ }
868
664
  }