chainlesschain 0.45.66 → 0.45.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/gateways/ws/message-dispatcher.js +12 -0
- package/src/gateways/ws/session-protocol.js +757 -0
- package/src/lib/abort-utils.js +20 -0
- package/src/lib/agent-core.js +83 -4
- package/src/lib/interaction-adapter.js +40 -21
- package/src/lib/sub-agent-registry.js +34 -0
- package/src/lib/ws-agent-handler.js +48 -0
- package/src/lib/ws-server.js +66 -0
- package/src/lib/ws-session-manager.js +369 -0
- package/src/runtime/coding-agent-events.cjs +31 -0
|
@@ -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,697 @@ 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
|
+
|
|
410
1167
|
export function handleHostToolResult(server, id, ws, message) {
|
|
411
1168
|
const { sessionId, requestId, success, result, error, toolName } = message;
|
|
412
1169
|
|