chainlesschain 0.45.66 → 0.45.70

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.
@@ -236,6 +236,25 @@ export function handleSessionMessage(server, id, ws, message) {
236
236
  return;
237
237
  }
238
238
 
239
+ // Review-mode gate: while the session has a pending blocking review the
240
+ // user must resolve it (approve/reject) before any new agent turn runs.
241
+ if (
242
+ server.sessionManager &&
243
+ typeof server.sessionManager.isReviewBlocking === "function" &&
244
+ server.sessionManager.isReviewBlocking(sessionId)
245
+ ) {
246
+ server._send(
247
+ ws,
248
+ envelopeError(
249
+ id,
250
+ "REVIEW_BLOCKING",
251
+ "Session is in review mode — resolve the pending review before sending new messages.",
252
+ sessionId,
253
+ ),
254
+ );
255
+ return;
256
+ }
257
+
239
258
  server.emit(
240
259
  RUNTIME_EVENTS.SESSION_MESSAGE,
241
260
  createRuntimeEvent(
@@ -380,6 +399,53 @@ export function handleSessionClose(server, id, ws, message) {
380
399
  );
381
400
  }
382
401
 
402
+ export async function handleSessionInterrupt(server, id, ws, message) {
403
+ const { sessionId } = message;
404
+
405
+ if (!server.sessionManager) {
406
+ server._send(
407
+ ws,
408
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
409
+ );
410
+ return;
411
+ }
412
+
413
+ const session = server.sessionManager.getSession(sessionId);
414
+ if (!session) {
415
+ server._send(
416
+ ws,
417
+ envelopeError(
418
+ id,
419
+ "SESSION_NOT_FOUND",
420
+ `Session not found: ${sessionId}`,
421
+ sessionId,
422
+ ),
423
+ );
424
+ return;
425
+ }
426
+
427
+ const handler = server.sessionHandlers.get(sessionId);
428
+ const result =
429
+ handler && typeof handler.interrupt === "function"
430
+ ? await handler.interrupt()
431
+ : {
432
+ sessionId,
433
+ interrupted: true,
434
+ wasProcessing: false,
435
+ interruptedRequestId: null,
436
+ };
437
+
438
+ server._send(
439
+ ws,
440
+ envelopeResponse(
441
+ CODING_AGENT_EVENT_TYPES.SESSION_INTERRUPTED,
442
+ id,
443
+ result,
444
+ sessionId,
445
+ ),
446
+ );
447
+ }
448
+
383
449
  export function handleSessionAnswer(server, id, ws, message) {
384
450
  const { sessionId, requestId, answer } = message;
385
451
 
@@ -407,6 +473,1064 @@ export function handleSessionAnswer(server, id, ws, message) {
407
473
  );
408
474
  }
409
475
 
476
+ /**
477
+ * Query sub-agents spawned from a session.
478
+ *
479
+ * Message shape: { type: "sub-agent-list", id, sessionId }
480
+ * Returns: envelope with payload { sessionId, active: [...], history: [...] }
481
+ *
482
+ * If `sessionId` is omitted, returns the global registry view so diagnostic
483
+ * tools (e.g. `chainlesschain tasks list --sub-agents`) can inspect every
484
+ * active child agent in the runtime.
485
+ */
486
+ export async function handleSubAgentList(server, id, ws, message) {
487
+ const sessionId = message?.sessionId || null;
488
+
489
+ try {
490
+ const { SubAgentRegistry } =
491
+ await import("../../lib/sub-agent-registry.js");
492
+ const registry = SubAgentRegistry.getInstance();
493
+
494
+ let payload;
495
+ if (sessionId) {
496
+ const scoped = registry.getByParent(sessionId);
497
+ payload = {
498
+ sessionId,
499
+ active: scoped.active,
500
+ history: scoped.history,
501
+ stats: registry.getStats(),
502
+ };
503
+ } else {
504
+ payload = {
505
+ sessionId: null,
506
+ active: registry.getActive(),
507
+ history: registry.getHistory(),
508
+ stats: registry.getStats(),
509
+ };
510
+ }
511
+
512
+ server._send(
513
+ ws,
514
+ envelopeResponse(
515
+ CODING_AGENT_EVENT_TYPES.SUB_AGENT_LIST,
516
+ id,
517
+ payload,
518
+ sessionId,
519
+ ),
520
+ );
521
+ } catch (err) {
522
+ server._send(
523
+ ws,
524
+ envelopeError(id, "SUB_AGENT_LIST_FAILED", err.message, sessionId),
525
+ );
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Fetch a single sub-agent snapshot by id.
531
+ *
532
+ * Message shape: { type: "sub-agent-get", id, subAgentId, sessionId? }
533
+ * Returns: envelope carrying the registry snapshot (active or history) or
534
+ * an error envelope when the id is unknown.
535
+ */
536
+ export async function handleSubAgentGet(server, id, ws, message) {
537
+ const { subAgentId, sessionId } = message || {};
538
+
539
+ if (!subAgentId) {
540
+ server._send(
541
+ ws,
542
+ envelopeError(
543
+ id,
544
+ "MISSING_SUB_AGENT_ID",
545
+ "sub-agent-get requires a subAgentId",
546
+ sessionId || null,
547
+ ),
548
+ );
549
+ return;
550
+ }
551
+
552
+ try {
553
+ const { SubAgentRegistry } =
554
+ await import("../../lib/sub-agent-registry.js");
555
+ const snapshot = SubAgentRegistry.getInstance().getById(subAgentId);
556
+
557
+ if (!snapshot) {
558
+ server._send(
559
+ ws,
560
+ envelopeError(
561
+ id,
562
+ "SUB_AGENT_NOT_FOUND",
563
+ `Sub-agent not found: ${subAgentId}`,
564
+ sessionId || null,
565
+ ),
566
+ );
567
+ return;
568
+ }
569
+
570
+ server._send(
571
+ ws,
572
+ envelopeResponse(
573
+ CODING_AGENT_EVENT_TYPES.SUB_AGENT_LIST,
574
+ id,
575
+ {
576
+ sessionId: sessionId || snapshot.parentId || null,
577
+ subAgent: snapshot,
578
+ },
579
+ sessionId || snapshot.parentId || null,
580
+ ),
581
+ );
582
+ } catch (err) {
583
+ server._send(
584
+ ws,
585
+ envelopeError(id, "SUB_AGENT_GET_FAILED", err.message, sessionId || null),
586
+ );
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Helper: emit a review.* envelope through the session's interaction adapter
592
+ * so every subscriber (bridge, renderer store) receives the same event
593
+ * stream other runtime events use. Falls back to directly sending over the
594
+ * current ws if the session has no interaction bound yet.
595
+ */
596
+ function _emitReviewEvent(server, session, type, payload, ws) {
597
+ const envelope = createCodingAgentEvent(
598
+ type,
599
+ { ...(payload || {}), sessionId: session.id },
600
+ {
601
+ sessionId: session.id,
602
+ source: "cli-runtime",
603
+ },
604
+ );
605
+
606
+ const interaction = session && session.interaction;
607
+ if (interaction && typeof interaction.emit === "function") {
608
+ try {
609
+ interaction.emit(type, envelope.payload);
610
+ return;
611
+ } catch (_err) {
612
+ // Fall through to ws send below.
613
+ }
614
+ }
615
+
616
+ if (ws) {
617
+ server._send(ws, envelope);
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Enter review mode — block sendMessage until the review is resolved.
623
+ *
624
+ * Message shape:
625
+ * { type: "review-enter", id, sessionId, reason?, requestedBy?, checklist?, blocking? }
626
+ */
627
+ export function handleReviewEnter(server, id, ws, message) {
628
+ const { sessionId } = message || {};
629
+
630
+ if (!server.sessionManager) {
631
+ server._send(
632
+ ws,
633
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
634
+ );
635
+ return;
636
+ }
637
+
638
+ const session = server.sessionManager.getSession(sessionId);
639
+ if (!session) {
640
+ server._send(
641
+ ws,
642
+ envelopeError(
643
+ id,
644
+ "SESSION_NOT_FOUND",
645
+ `Session not found: ${sessionId}`,
646
+ sessionId,
647
+ ),
648
+ );
649
+ return;
650
+ }
651
+
652
+ const reviewState = server.sessionManager.enterReview(sessionId, {
653
+ reason: message.reason || null,
654
+ requestedBy: message.requestedBy || "user",
655
+ checklist: message.checklist || [],
656
+ blocking: message.blocking !== false,
657
+ });
658
+
659
+ server._send(
660
+ ws,
661
+ envelopeResponse(
662
+ CODING_AGENT_EVENT_TYPES.REVIEW_REQUESTED,
663
+ id,
664
+ { sessionId, reviewState },
665
+ sessionId,
666
+ ),
667
+ );
668
+
669
+ _emitReviewEvent(
670
+ server,
671
+ session,
672
+ CODING_AGENT_EVENT_TYPES.REVIEW_REQUESTED,
673
+ { reviewState },
674
+ ws,
675
+ );
676
+ }
677
+
678
+ /**
679
+ * Submit a comment or toggle a checklist item on the active review.
680
+ *
681
+ * Message shape:
682
+ * { type: "review-submit", id, sessionId,
683
+ * comment?: { author?, content },
684
+ * checklistItemId?, checklistItemDone?, checklistItemNote? }
685
+ */
686
+ export function handleReviewSubmit(server, id, ws, message) {
687
+ const { sessionId } = message || {};
688
+
689
+ if (!server.sessionManager) {
690
+ server._send(
691
+ ws,
692
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
693
+ );
694
+ return;
695
+ }
696
+
697
+ const session = server.sessionManager.getSession(sessionId);
698
+ if (!session) {
699
+ server._send(
700
+ ws,
701
+ envelopeError(
702
+ id,
703
+ "SESSION_NOT_FOUND",
704
+ `Session not found: ${sessionId}`,
705
+ sessionId,
706
+ ),
707
+ );
708
+ return;
709
+ }
710
+
711
+ const updated = server.sessionManager.submitReviewComment(sessionId, {
712
+ comment: message.comment || null,
713
+ checklistItemId: message.checklistItemId || null,
714
+ checklistItemDone: message.checklistItemDone,
715
+ checklistItemNote: message.checklistItemNote,
716
+ });
717
+
718
+ if (!updated) {
719
+ server._send(
720
+ ws,
721
+ envelopeError(
722
+ id,
723
+ "REVIEW_NOT_PENDING",
724
+ "No pending review for this session",
725
+ sessionId,
726
+ ),
727
+ );
728
+ return;
729
+ }
730
+
731
+ server._send(
732
+ ws,
733
+ envelopeResponse(
734
+ CODING_AGENT_EVENT_TYPES.REVIEW_UPDATED,
735
+ id,
736
+ { sessionId, reviewState: updated },
737
+ sessionId,
738
+ ),
739
+ );
740
+
741
+ _emitReviewEvent(
742
+ server,
743
+ session,
744
+ CODING_AGENT_EVENT_TYPES.REVIEW_UPDATED,
745
+ { reviewState: updated },
746
+ ws,
747
+ );
748
+ }
749
+
750
+ /**
751
+ * Resolve the active review with approved/rejected. Unblocks sendMessage.
752
+ *
753
+ * Message shape:
754
+ * { type: "review-resolve", id, sessionId, decision, resolvedBy?, summary? }
755
+ */
756
+ export function handleReviewResolve(server, id, ws, message) {
757
+ const { sessionId } = message || {};
758
+
759
+ if (!server.sessionManager) {
760
+ server._send(
761
+ ws,
762
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
763
+ );
764
+ return;
765
+ }
766
+
767
+ const session = server.sessionManager.getSession(sessionId);
768
+ if (!session) {
769
+ server._send(
770
+ ws,
771
+ envelopeError(
772
+ id,
773
+ "SESSION_NOT_FOUND",
774
+ `Session not found: ${sessionId}`,
775
+ sessionId,
776
+ ),
777
+ );
778
+ return;
779
+ }
780
+
781
+ const resolved = server.sessionManager.resolveReview(sessionId, {
782
+ decision: message.decision,
783
+ resolvedBy: message.resolvedBy || "user",
784
+ summary: message.summary || null,
785
+ });
786
+
787
+ if (!resolved) {
788
+ server._send(
789
+ ws,
790
+ envelopeError(
791
+ id,
792
+ "REVIEW_NOT_PENDING",
793
+ "No pending review for this session",
794
+ sessionId,
795
+ ),
796
+ );
797
+ return;
798
+ }
799
+
800
+ server._send(
801
+ ws,
802
+ envelopeResponse(
803
+ CODING_AGENT_EVENT_TYPES.REVIEW_RESOLVED,
804
+ id,
805
+ { sessionId, reviewState: resolved },
806
+ sessionId,
807
+ ),
808
+ );
809
+
810
+ _emitReviewEvent(
811
+ server,
812
+ session,
813
+ CODING_AGENT_EVENT_TYPES.REVIEW_RESOLVED,
814
+ { reviewState: resolved },
815
+ ws,
816
+ );
817
+ }
818
+
819
+ /**
820
+ * Fetch the current review state snapshot (or null if none).
821
+ *
822
+ * Message shape: { type: "review-status", id, sessionId }
823
+ */
824
+ export function handleReviewStatus(server, id, ws, message) {
825
+ const { sessionId } = message || {};
826
+
827
+ if (!server.sessionManager) {
828
+ server._send(
829
+ ws,
830
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
831
+ );
832
+ return;
833
+ }
834
+
835
+ const session = server.sessionManager.getSession(sessionId);
836
+ if (!session) {
837
+ server._send(
838
+ ws,
839
+ envelopeError(
840
+ id,
841
+ "SESSION_NOT_FOUND",
842
+ `Session not found: ${sessionId}`,
843
+ sessionId,
844
+ ),
845
+ );
846
+ return;
847
+ }
848
+
849
+ server._send(
850
+ ws,
851
+ envelopeResponse(
852
+ CODING_AGENT_EVENT_TYPES.REVIEW_STATE,
853
+ id,
854
+ { sessionId, reviewState: session.reviewState || null },
855
+ sessionId,
856
+ ),
857
+ );
858
+ }
859
+
860
+ /**
861
+ * Helper: emit a patch.* envelope through the session's interaction adapter
862
+ * (same fan-out pattern as _emitReviewEvent).
863
+ */
864
+ function _emitPatchEvent(server, session, type, payload, ws) {
865
+ const envelope = createCodingAgentEvent(
866
+ type,
867
+ { ...(payload || {}), sessionId: session.id },
868
+ {
869
+ sessionId: session.id,
870
+ source: "cli-runtime",
871
+ },
872
+ );
873
+
874
+ const interaction = session && session.interaction;
875
+ if (interaction && typeof interaction.emit === "function") {
876
+ try {
877
+ interaction.emit(type, envelope.payload);
878
+ return;
879
+ } catch (_err) {
880
+ // Fall through to ws send below.
881
+ }
882
+ }
883
+
884
+ if (ws) {
885
+ server._send(ws, envelope);
886
+ }
887
+ }
888
+
889
+ /**
890
+ * Propose a patch (or batch of file edits) for preview.
891
+ *
892
+ * Message shape:
893
+ * { type: "patch-propose", id, sessionId, files: [...], origin?, reason? }
894
+ */
895
+ export function handlePatchPropose(server, id, ws, message) {
896
+ const { sessionId } = message || {};
897
+
898
+ if (!server.sessionManager) {
899
+ server._send(
900
+ ws,
901
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
902
+ );
903
+ return;
904
+ }
905
+
906
+ const session = server.sessionManager.getSession(sessionId);
907
+ if (!session) {
908
+ server._send(
909
+ ws,
910
+ envelopeError(
911
+ id,
912
+ "SESSION_NOT_FOUND",
913
+ `Session not found: ${sessionId}`,
914
+ sessionId,
915
+ ),
916
+ );
917
+ return;
918
+ }
919
+
920
+ if (!Array.isArray(message.files) || message.files.length === 0) {
921
+ server._send(
922
+ ws,
923
+ envelopeError(
924
+ id,
925
+ "INVALID_PAYLOAD",
926
+ "patch-propose requires a non-empty files array",
927
+ sessionId,
928
+ ),
929
+ );
930
+ return;
931
+ }
932
+
933
+ const patch = server.sessionManager.proposePatch(sessionId, {
934
+ files: message.files,
935
+ origin: message.origin || "tool",
936
+ reason: message.reason || null,
937
+ requestId: message.requestId || null,
938
+ });
939
+
940
+ if (!patch) {
941
+ server._send(
942
+ ws,
943
+ envelopeError(
944
+ id,
945
+ "PATCH_PROPOSE_FAILED",
946
+ "Unable to record patch",
947
+ sessionId,
948
+ ),
949
+ );
950
+ return;
951
+ }
952
+
953
+ server._send(
954
+ ws,
955
+ envelopeResponse(
956
+ CODING_AGENT_EVENT_TYPES.PATCH_PROPOSED,
957
+ id,
958
+ { sessionId, patch },
959
+ sessionId,
960
+ ),
961
+ );
962
+
963
+ _emitPatchEvent(
964
+ server,
965
+ session,
966
+ CODING_AGENT_EVENT_TYPES.PATCH_PROPOSED,
967
+ { patch },
968
+ ws,
969
+ );
970
+ }
971
+
972
+ /**
973
+ * Apply a previously-proposed patch.
974
+ *
975
+ * Message shape:
976
+ * { type: "patch-apply", id, sessionId, patchId, resolvedBy?, note? }
977
+ */
978
+ export function handlePatchApply(server, id, ws, message) {
979
+ const { sessionId, patchId } = message || {};
980
+
981
+ if (!server.sessionManager) {
982
+ server._send(
983
+ ws,
984
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
985
+ );
986
+ return;
987
+ }
988
+
989
+ const session = server.sessionManager.getSession(sessionId);
990
+ if (!session) {
991
+ server._send(
992
+ ws,
993
+ envelopeError(
994
+ id,
995
+ "SESSION_NOT_FOUND",
996
+ `Session not found: ${sessionId}`,
997
+ sessionId,
998
+ ),
999
+ );
1000
+ return;
1001
+ }
1002
+
1003
+ if (!patchId) {
1004
+ server._send(
1005
+ ws,
1006
+ envelopeError(id, "INVALID_PAYLOAD", "patchId is required", sessionId),
1007
+ );
1008
+ return;
1009
+ }
1010
+
1011
+ const patch = server.sessionManager.applyPatch(sessionId, patchId, {
1012
+ resolvedBy: message.resolvedBy || "user",
1013
+ note: message.note || null,
1014
+ });
1015
+
1016
+ if (!patch) {
1017
+ server._send(
1018
+ ws,
1019
+ envelopeError(
1020
+ id,
1021
+ "PATCH_NOT_FOUND",
1022
+ `Patch not found: ${patchId}`,
1023
+ sessionId,
1024
+ ),
1025
+ );
1026
+ return;
1027
+ }
1028
+
1029
+ server._send(
1030
+ ws,
1031
+ envelopeResponse(
1032
+ CODING_AGENT_EVENT_TYPES.PATCH_APPLIED,
1033
+ id,
1034
+ { sessionId, patch },
1035
+ sessionId,
1036
+ ),
1037
+ );
1038
+
1039
+ _emitPatchEvent(
1040
+ server,
1041
+ session,
1042
+ CODING_AGENT_EVENT_TYPES.PATCH_APPLIED,
1043
+ { patch },
1044
+ ws,
1045
+ );
1046
+ }
1047
+
1048
+ /**
1049
+ * Reject/discard a previously-proposed patch.
1050
+ *
1051
+ * Message shape:
1052
+ * { type: "patch-reject", id, sessionId, patchId, resolvedBy?, reason? }
1053
+ */
1054
+ export function handlePatchReject(server, id, ws, message) {
1055
+ const { sessionId, patchId } = message || {};
1056
+
1057
+ if (!server.sessionManager) {
1058
+ server._send(
1059
+ ws,
1060
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1061
+ );
1062
+ return;
1063
+ }
1064
+
1065
+ const session = server.sessionManager.getSession(sessionId);
1066
+ if (!session) {
1067
+ server._send(
1068
+ ws,
1069
+ envelopeError(
1070
+ id,
1071
+ "SESSION_NOT_FOUND",
1072
+ `Session not found: ${sessionId}`,
1073
+ sessionId,
1074
+ ),
1075
+ );
1076
+ return;
1077
+ }
1078
+
1079
+ if (!patchId) {
1080
+ server._send(
1081
+ ws,
1082
+ envelopeError(id, "INVALID_PAYLOAD", "patchId is required", sessionId),
1083
+ );
1084
+ return;
1085
+ }
1086
+
1087
+ const patch = server.sessionManager.rejectPatch(sessionId, patchId, {
1088
+ resolvedBy: message.resolvedBy || "user",
1089
+ reason: message.reason || null,
1090
+ });
1091
+
1092
+ if (!patch) {
1093
+ server._send(
1094
+ ws,
1095
+ envelopeError(
1096
+ id,
1097
+ "PATCH_NOT_FOUND",
1098
+ `Patch not found: ${patchId}`,
1099
+ sessionId,
1100
+ ),
1101
+ );
1102
+ return;
1103
+ }
1104
+
1105
+ server._send(
1106
+ ws,
1107
+ envelopeResponse(
1108
+ CODING_AGENT_EVENT_TYPES.PATCH_REJECTED,
1109
+ id,
1110
+ { sessionId, patch },
1111
+ sessionId,
1112
+ ),
1113
+ );
1114
+
1115
+ _emitPatchEvent(
1116
+ server,
1117
+ session,
1118
+ CODING_AGENT_EVENT_TYPES.PATCH_REJECTED,
1119
+ { patch },
1120
+ ws,
1121
+ );
1122
+ }
1123
+
1124
+ /**
1125
+ * Fetch the patch summary for a session (pending + history + totals).
1126
+ *
1127
+ * Message shape: { type: "patch-summary", id, sessionId }
1128
+ */
1129
+ export function handlePatchSummary(server, id, ws, message) {
1130
+ const { sessionId } = message || {};
1131
+
1132
+ if (!server.sessionManager) {
1133
+ server._send(
1134
+ ws,
1135
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1136
+ );
1137
+ return;
1138
+ }
1139
+
1140
+ const session = server.sessionManager.getSession(sessionId);
1141
+ if (!session) {
1142
+ server._send(
1143
+ ws,
1144
+ envelopeError(
1145
+ id,
1146
+ "SESSION_NOT_FOUND",
1147
+ `Session not found: ${sessionId}`,
1148
+ sessionId,
1149
+ ),
1150
+ );
1151
+ return;
1152
+ }
1153
+
1154
+ const summary = server.sessionManager.getPatchSummary(sessionId);
1155
+
1156
+ server._send(
1157
+ ws,
1158
+ envelopeResponse(
1159
+ CODING_AGENT_EVENT_TYPES.PATCH_SUMMARY,
1160
+ id,
1161
+ { sessionId, summary },
1162
+ sessionId,
1163
+ ),
1164
+ );
1165
+ }
1166
+
1167
+ /**
1168
+ * Helper: emit a task-graph.* envelope through the session's interaction
1169
+ * adapter (same fan-out pattern as _emitPatchEvent).
1170
+ */
1171
+ function _emitTaskGraphEvent(server, session, type, payload, ws) {
1172
+ const envelope = createCodingAgentEvent(
1173
+ type,
1174
+ { ...(payload || {}), sessionId: session.id },
1175
+ {
1176
+ sessionId: session.id,
1177
+ source: "cli-runtime",
1178
+ },
1179
+ );
1180
+
1181
+ const interaction = session && session.interaction;
1182
+ if (interaction && typeof interaction.emit === "function") {
1183
+ try {
1184
+ interaction.emit(type, envelope.payload);
1185
+ return;
1186
+ } catch (_err) {
1187
+ // Fall through to ws send below.
1188
+ }
1189
+ }
1190
+
1191
+ if (ws) {
1192
+ server._send(ws, envelope);
1193
+ }
1194
+ }
1195
+
1196
+ /**
1197
+ * Create a session-scoped task graph.
1198
+ *
1199
+ * Message shape:
1200
+ * { type: "task-graph-create", id, sessionId, title?, nodes: [...] }
1201
+ */
1202
+ export function handleTaskGraphCreate(server, id, ws, message) {
1203
+ const { sessionId } = message || {};
1204
+
1205
+ if (!server.sessionManager) {
1206
+ server._send(
1207
+ ws,
1208
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1209
+ );
1210
+ return;
1211
+ }
1212
+
1213
+ const session = server.sessionManager.getSession(sessionId);
1214
+ if (!session) {
1215
+ server._send(
1216
+ ws,
1217
+ envelopeError(
1218
+ id,
1219
+ "SESSION_NOT_FOUND",
1220
+ `Session not found: ${sessionId}`,
1221
+ sessionId,
1222
+ ),
1223
+ );
1224
+ return;
1225
+ }
1226
+
1227
+ if (!Array.isArray(message.nodes)) {
1228
+ server._send(
1229
+ ws,
1230
+ envelopeError(
1231
+ id,
1232
+ "INVALID_PAYLOAD",
1233
+ "task-graph-create requires a nodes array",
1234
+ sessionId,
1235
+ ),
1236
+ );
1237
+ return;
1238
+ }
1239
+
1240
+ const graph = server.sessionManager.createTaskGraph(sessionId, {
1241
+ graphId: message.graphId,
1242
+ title: message.title,
1243
+ description: message.description,
1244
+ nodes: message.nodes,
1245
+ });
1246
+
1247
+ server._send(
1248
+ ws,
1249
+ envelopeResponse(
1250
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_CREATED,
1251
+ id,
1252
+ { sessionId, graph },
1253
+ sessionId,
1254
+ ),
1255
+ );
1256
+
1257
+ _emitTaskGraphEvent(
1258
+ server,
1259
+ session,
1260
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_CREATED,
1261
+ { graph },
1262
+ ws,
1263
+ );
1264
+ }
1265
+
1266
+ /**
1267
+ * Add a node to an existing task graph.
1268
+ *
1269
+ * Message shape:
1270
+ * { type: "task-graph-add-node", id, sessionId, node: { id, title, dependsOn? } }
1271
+ */
1272
+ export function handleTaskGraphAddNode(server, id, ws, message) {
1273
+ const { sessionId } = message || {};
1274
+
1275
+ if (!server.sessionManager) {
1276
+ server._send(
1277
+ ws,
1278
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1279
+ );
1280
+ return;
1281
+ }
1282
+
1283
+ const session = server.sessionManager.getSession(sessionId);
1284
+ if (!session) {
1285
+ server._send(
1286
+ ws,
1287
+ envelopeError(
1288
+ id,
1289
+ "SESSION_NOT_FOUND",
1290
+ `Session not found: ${sessionId}`,
1291
+ sessionId,
1292
+ ),
1293
+ );
1294
+ return;
1295
+ }
1296
+
1297
+ const node = message.node || null;
1298
+ if (!node || !node.id) {
1299
+ server._send(
1300
+ ws,
1301
+ envelopeError(
1302
+ id,
1303
+ "INVALID_PAYLOAD",
1304
+ "task-graph-add-node requires node.id",
1305
+ sessionId,
1306
+ ),
1307
+ );
1308
+ return;
1309
+ }
1310
+
1311
+ const graph = server.sessionManager.addTaskNode(sessionId, node);
1312
+ if (!graph) {
1313
+ server._send(
1314
+ ws,
1315
+ envelopeError(
1316
+ id,
1317
+ "TASK_GRAPH_ADD_FAILED",
1318
+ "Unable to add node (no graph, or duplicate id)",
1319
+ sessionId,
1320
+ ),
1321
+ );
1322
+ return;
1323
+ }
1324
+
1325
+ server._send(
1326
+ ws,
1327
+ envelopeResponse(
1328
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_ADDED,
1329
+ id,
1330
+ { sessionId, graph, nodeId: node.id },
1331
+ sessionId,
1332
+ ),
1333
+ );
1334
+
1335
+ _emitTaskGraphEvent(
1336
+ server,
1337
+ session,
1338
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_ADDED,
1339
+ { graph, nodeId: node.id },
1340
+ ws,
1341
+ );
1342
+ }
1343
+
1344
+ /**
1345
+ * Update a task graph node (status, result, error, metadata).
1346
+ *
1347
+ * Message shape:
1348
+ * { type: "task-graph-update-node", id, sessionId, nodeId, updates: { status?, result?, error? } }
1349
+ */
1350
+ export function handleTaskGraphUpdateNode(server, id, ws, message) {
1351
+ const { sessionId, nodeId } = message || {};
1352
+
1353
+ if (!server.sessionManager) {
1354
+ server._send(
1355
+ ws,
1356
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1357
+ );
1358
+ return;
1359
+ }
1360
+
1361
+ const session = server.sessionManager.getSession(sessionId);
1362
+ if (!session) {
1363
+ server._send(
1364
+ ws,
1365
+ envelopeError(
1366
+ id,
1367
+ "SESSION_NOT_FOUND",
1368
+ `Session not found: ${sessionId}`,
1369
+ sessionId,
1370
+ ),
1371
+ );
1372
+ return;
1373
+ }
1374
+
1375
+ if (!nodeId) {
1376
+ server._send(
1377
+ ws,
1378
+ envelopeError(id, "INVALID_PAYLOAD", "nodeId is required", sessionId),
1379
+ );
1380
+ return;
1381
+ }
1382
+
1383
+ const graph = server.sessionManager.updateTaskNode(
1384
+ sessionId,
1385
+ nodeId,
1386
+ message.updates || {},
1387
+ );
1388
+
1389
+ if (!graph) {
1390
+ server._send(
1391
+ ws,
1392
+ envelopeError(
1393
+ id,
1394
+ "TASK_GRAPH_NODE_NOT_FOUND",
1395
+ `Task node not found: ${nodeId}`,
1396
+ sessionId,
1397
+ ),
1398
+ );
1399
+ return;
1400
+ }
1401
+
1402
+ const node = graph.nodes[nodeId];
1403
+ let eventType = CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_UPDATED;
1404
+ if (node && node.status === "completed") {
1405
+ eventType = CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_COMPLETED;
1406
+ } else if (node && node.status === "failed") {
1407
+ eventType = CODING_AGENT_EVENT_TYPES.TASK_GRAPH_NODE_FAILED;
1408
+ }
1409
+
1410
+ server._send(
1411
+ ws,
1412
+ envelopeResponse(eventType, id, { sessionId, graph, nodeId }, sessionId),
1413
+ );
1414
+
1415
+ _emitTaskGraphEvent(server, session, eventType, { graph, nodeId }, ws);
1416
+
1417
+ if (graph.status === "completed" || graph.status === "failed") {
1418
+ _emitTaskGraphEvent(
1419
+ server,
1420
+ session,
1421
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_COMPLETED,
1422
+ { graph },
1423
+ ws,
1424
+ );
1425
+ }
1426
+ }
1427
+
1428
+ /**
1429
+ * Advance the task graph: promote any pending node whose deps are satisfied.
1430
+ *
1431
+ * Message shape: { type: "task-graph-advance", id, sessionId }
1432
+ */
1433
+ export function handleTaskGraphAdvance(server, id, ws, message) {
1434
+ const { sessionId } = message || {};
1435
+
1436
+ if (!server.sessionManager) {
1437
+ server._send(
1438
+ ws,
1439
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1440
+ );
1441
+ return;
1442
+ }
1443
+
1444
+ const session = server.sessionManager.getSession(sessionId);
1445
+ if (!session) {
1446
+ server._send(
1447
+ ws,
1448
+ envelopeError(
1449
+ id,
1450
+ "SESSION_NOT_FOUND",
1451
+ `Session not found: ${sessionId}`,
1452
+ sessionId,
1453
+ ),
1454
+ );
1455
+ return;
1456
+ }
1457
+
1458
+ const result = server.sessionManager.advanceTaskGraph(sessionId);
1459
+ if (!result) {
1460
+ server._send(
1461
+ ws,
1462
+ envelopeError(
1463
+ id,
1464
+ "TASK_GRAPH_NOT_FOUND",
1465
+ "No task graph on session",
1466
+ sessionId,
1467
+ ),
1468
+ );
1469
+ return;
1470
+ }
1471
+
1472
+ server._send(
1473
+ ws,
1474
+ envelopeResponse(
1475
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_ADVANCED,
1476
+ id,
1477
+ { sessionId, graph: result.graph, becameReady: result.becameReady },
1478
+ sessionId,
1479
+ ),
1480
+ );
1481
+
1482
+ _emitTaskGraphEvent(
1483
+ server,
1484
+ session,
1485
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_ADVANCED,
1486
+ { graph: result.graph, becameReady: result.becameReady },
1487
+ ws,
1488
+ );
1489
+ }
1490
+
1491
+ /**
1492
+ * Fetch the current task graph state.
1493
+ *
1494
+ * Message shape: { type: "task-graph-state", id, sessionId }
1495
+ */
1496
+ export function handleTaskGraphState(server, id, ws, message) {
1497
+ const { sessionId } = message || {};
1498
+
1499
+ if (!server.sessionManager) {
1500
+ server._send(
1501
+ ws,
1502
+ envelopeError(id, "NO_SESSION_SUPPORT", "Session support not configured"),
1503
+ );
1504
+ return;
1505
+ }
1506
+
1507
+ const session = server.sessionManager.getSession(sessionId);
1508
+ if (!session) {
1509
+ server._send(
1510
+ ws,
1511
+ envelopeError(
1512
+ id,
1513
+ "SESSION_NOT_FOUND",
1514
+ `Session not found: ${sessionId}`,
1515
+ sessionId,
1516
+ ),
1517
+ );
1518
+ return;
1519
+ }
1520
+
1521
+ const graph = server.sessionManager.getTaskGraph(sessionId);
1522
+
1523
+ server._send(
1524
+ ws,
1525
+ envelopeResponse(
1526
+ CODING_AGENT_EVENT_TYPES.TASK_GRAPH_STATE,
1527
+ id,
1528
+ { sessionId, graph },
1529
+ sessionId,
1530
+ ),
1531
+ );
1532
+ }
1533
+
410
1534
  export function handleHostToolResult(server, id, ws, message) {
411
1535
  const { sessionId, requestId, success, result, error, toolName } = message;
412
1536