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
|
@@ -349,6 +349,9 @@ export class WSSessionManager {
|
|
|
349
349
|
planManager,
|
|
350
350
|
contextEngine,
|
|
351
351
|
permanentMemory,
|
|
352
|
+
reviewState: null,
|
|
353
|
+
pendingPatches: new Map(),
|
|
354
|
+
patchHistory: [],
|
|
352
355
|
interaction: null, // Set by ws-server after creation
|
|
353
356
|
createdAt: new Date().toISOString(),
|
|
354
357
|
lastActivity: new Date().toISOString(),
|
|
@@ -456,6 +459,11 @@ export class WSSessionManager {
|
|
|
456
459
|
planManager,
|
|
457
460
|
contextEngine,
|
|
458
461
|
permanentMemory,
|
|
462
|
+
reviewState: metadata.reviewState || null,
|
|
463
|
+
pendingPatches: this._hydratePendingPatches(metadata.pendingPatches),
|
|
464
|
+
patchHistory: Array.isArray(metadata.patchHistory)
|
|
465
|
+
? metadata.patchHistory
|
|
466
|
+
: [],
|
|
459
467
|
interaction: null,
|
|
460
468
|
createdAt: dbSession.created_at,
|
|
461
469
|
lastActivity: new Date().toISOString(),
|
|
@@ -611,6 +619,347 @@ export class WSSessionManager {
|
|
|
611
619
|
return session;
|
|
612
620
|
}
|
|
613
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Enter explicit review mode for a session. While in review, handlers
|
|
624
|
+
* MUST gate new sendMessage calls until the review is resolved. Reviewer
|
|
625
|
+
* sub-agents and human reviewers both feed into the same `comments` /
|
|
626
|
+
* `checklist` arrays.
|
|
627
|
+
*
|
|
628
|
+
* @param {string} sessionId
|
|
629
|
+
* @param {{
|
|
630
|
+
* reason?: string,
|
|
631
|
+
* requestedBy?: string,
|
|
632
|
+
* checklist?: Array<{ id?: string, title: string, note?: string }>,
|
|
633
|
+
* blocking?: boolean,
|
|
634
|
+
* }} [options]
|
|
635
|
+
*/
|
|
636
|
+
enterReview(sessionId, options = {}) {
|
|
637
|
+
const session = this.sessions.get(sessionId);
|
|
638
|
+
if (!session) return null;
|
|
639
|
+
|
|
640
|
+
// If already in pending review, return the existing state unchanged so
|
|
641
|
+
// callers can retry safely.
|
|
642
|
+
if (session.reviewState && session.reviewState.status === "pending") {
|
|
643
|
+
return session.reviewState;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const reviewId = `review-${this._generateId()}`;
|
|
647
|
+
const now = new Date().toISOString();
|
|
648
|
+
const checklist = Array.isArray(options.checklist)
|
|
649
|
+
? options.checklist.map((item, index) => ({
|
|
650
|
+
id: item.id || `chk-${index}-${Date.now()}`,
|
|
651
|
+
title: item.title || `Item ${index + 1}`,
|
|
652
|
+
note: item.note || null,
|
|
653
|
+
done: false,
|
|
654
|
+
}))
|
|
655
|
+
: [];
|
|
656
|
+
|
|
657
|
+
session.reviewState = {
|
|
658
|
+
reviewId,
|
|
659
|
+
status: "pending",
|
|
660
|
+
reason: options.reason || null,
|
|
661
|
+
requestedBy: options.requestedBy || "user",
|
|
662
|
+
requestedAt: now,
|
|
663
|
+
resolvedAt: null,
|
|
664
|
+
resolvedBy: null,
|
|
665
|
+
decision: null,
|
|
666
|
+
blocking: options.blocking !== false,
|
|
667
|
+
comments: [],
|
|
668
|
+
checklist,
|
|
669
|
+
};
|
|
670
|
+
session.lastActivity = now;
|
|
671
|
+
this._persistSessionState(sessionId);
|
|
672
|
+
return session.reviewState;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Submit an incremental update to the active review — append a comment
|
|
677
|
+
* and/or toggle a checklist item. Returns the updated reviewState, or null
|
|
678
|
+
* if the session has no active review.
|
|
679
|
+
*
|
|
680
|
+
* @param {string} sessionId
|
|
681
|
+
* @param {{
|
|
682
|
+
* comment?: { author?: string, content: string },
|
|
683
|
+
* checklistItemId?: string,
|
|
684
|
+
* checklistItemDone?: boolean,
|
|
685
|
+
* checklistItemNote?: string,
|
|
686
|
+
* }} update
|
|
687
|
+
*/
|
|
688
|
+
submitReviewComment(sessionId, update = {}) {
|
|
689
|
+
const session = this.sessions.get(sessionId);
|
|
690
|
+
if (!session || !session.reviewState) return null;
|
|
691
|
+
if (session.reviewState.status !== "pending") return null;
|
|
692
|
+
|
|
693
|
+
const now = new Date().toISOString();
|
|
694
|
+
|
|
695
|
+
if (update.comment && update.comment.content) {
|
|
696
|
+
session.reviewState.comments.push({
|
|
697
|
+
id: `cmt-${session.reviewState.comments.length}-${Date.now()}`,
|
|
698
|
+
author: update.comment.author || "user",
|
|
699
|
+
content: String(update.comment.content),
|
|
700
|
+
timestamp: now,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (update.checklistItemId) {
|
|
705
|
+
const item = session.reviewState.checklist.find(
|
|
706
|
+
(c) => c.id === update.checklistItemId,
|
|
707
|
+
);
|
|
708
|
+
if (item) {
|
|
709
|
+
if (typeof update.checklistItemDone === "boolean") {
|
|
710
|
+
item.done = update.checklistItemDone;
|
|
711
|
+
}
|
|
712
|
+
if (typeof update.checklistItemNote === "string") {
|
|
713
|
+
item.note = update.checklistItemNote;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
session.lastActivity = now;
|
|
719
|
+
this._persistSessionState(sessionId);
|
|
720
|
+
return session.reviewState;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Resolve the active review with an approved/rejected decision. After
|
|
725
|
+
* resolve the session can accept new messages again (reviewState becomes
|
|
726
|
+
* non-blocking but is retained for audit).
|
|
727
|
+
*
|
|
728
|
+
* @param {string} sessionId
|
|
729
|
+
* @param {{ decision: "approved"|"rejected", resolvedBy?: string, summary?: string }} payload
|
|
730
|
+
*/
|
|
731
|
+
resolveReview(sessionId, payload = {}) {
|
|
732
|
+
const session = this.sessions.get(sessionId);
|
|
733
|
+
if (!session || !session.reviewState) return null;
|
|
734
|
+
if (session.reviewState.status !== "pending") {
|
|
735
|
+
return session.reviewState;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const decision =
|
|
739
|
+
payload.decision === "approved" || payload.decision === "rejected"
|
|
740
|
+
? payload.decision
|
|
741
|
+
: "approved";
|
|
742
|
+
|
|
743
|
+
session.reviewState.status = decision;
|
|
744
|
+
session.reviewState.decision = decision;
|
|
745
|
+
session.reviewState.resolvedAt = new Date().toISOString();
|
|
746
|
+
session.reviewState.resolvedBy = payload.resolvedBy || "user";
|
|
747
|
+
session.reviewState.blocking = false;
|
|
748
|
+
if (payload.summary) {
|
|
749
|
+
session.reviewState.summary = String(payload.summary);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
session.lastActivity = session.reviewState.resolvedAt;
|
|
753
|
+
this._persistSessionState(sessionId);
|
|
754
|
+
return session.reviewState;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Returns true when the session currently has a blocking review gate
|
|
759
|
+
* open. Callers (e.g. handleSessionMessage) should short-circuit with a
|
|
760
|
+
* REVIEW_BLOCKING error instead of running the agent turn.
|
|
761
|
+
*/
|
|
762
|
+
isReviewBlocking(sessionId) {
|
|
763
|
+
const session = this.sessions.get(sessionId);
|
|
764
|
+
if (!session || !session.reviewState) return false;
|
|
765
|
+
return (
|
|
766
|
+
session.reviewState.status === "pending" &&
|
|
767
|
+
session.reviewState.blocking === true
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
getReviewState(sessionId) {
|
|
772
|
+
const session = this.sessions.get(sessionId);
|
|
773
|
+
return session ? session.reviewState || null : null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Record a proposed patch on the session. Accepts one or more file hunks
|
|
778
|
+
* that a tool wanted to write but should be previewed before they land.
|
|
779
|
+
*
|
|
780
|
+
* @param {string} sessionId
|
|
781
|
+
* @param {{
|
|
782
|
+
* files: Array<{
|
|
783
|
+
* path: string,
|
|
784
|
+
* op?: "create"|"modify"|"delete",
|
|
785
|
+
* before?: string|null,
|
|
786
|
+
* after?: string|null,
|
|
787
|
+
* diff?: string|null,
|
|
788
|
+
* stats?: { added?: number, removed?: number }
|
|
789
|
+
* }>,
|
|
790
|
+
* origin?: string,
|
|
791
|
+
* reason?: string,
|
|
792
|
+
* requestId?: string|null
|
|
793
|
+
* }} payload
|
|
794
|
+
* @returns {object|null} patch record, or null if the session is missing
|
|
795
|
+
*/
|
|
796
|
+
proposePatch(sessionId, payload = {}) {
|
|
797
|
+
const session = this.sessions.get(sessionId);
|
|
798
|
+
if (!session) return null;
|
|
799
|
+
|
|
800
|
+
const files = Array.isArray(payload.files) ? payload.files : [];
|
|
801
|
+
if (files.length === 0) return null;
|
|
802
|
+
|
|
803
|
+
const patchId = `patch-${this._generateId()}`;
|
|
804
|
+
const now = new Date().toISOString();
|
|
805
|
+
const normalizedFiles = files.map((file, index) => {
|
|
806
|
+
const op = file.op || (file.before == null ? "create" : "modify");
|
|
807
|
+
const stats = this._computePatchStats(file);
|
|
808
|
+
return {
|
|
809
|
+
index,
|
|
810
|
+
path: file.path || `unknown-${index}`,
|
|
811
|
+
op,
|
|
812
|
+
before: file.before == null ? null : String(file.before),
|
|
813
|
+
after: file.after == null ? null : String(file.after),
|
|
814
|
+
diff: file.diff == null ? null : String(file.diff),
|
|
815
|
+
stats,
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const totalStats = normalizedFiles.reduce(
|
|
820
|
+
(acc, file) => ({
|
|
821
|
+
added: acc.added + (file.stats.added || 0),
|
|
822
|
+
removed: acc.removed + (file.stats.removed || 0),
|
|
823
|
+
}),
|
|
824
|
+
{ added: 0, removed: 0 },
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
const patch = {
|
|
828
|
+
patchId,
|
|
829
|
+
status: "pending",
|
|
830
|
+
origin: payload.origin || "tool",
|
|
831
|
+
reason: payload.reason || null,
|
|
832
|
+
requestId: payload.requestId || null,
|
|
833
|
+
proposedAt: now,
|
|
834
|
+
resolvedAt: null,
|
|
835
|
+
resolvedBy: null,
|
|
836
|
+
files: normalizedFiles,
|
|
837
|
+
stats: {
|
|
838
|
+
fileCount: normalizedFiles.length,
|
|
839
|
+
added: totalStats.added,
|
|
840
|
+
removed: totalStats.removed,
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
if (!(session.pendingPatches instanceof Map)) {
|
|
845
|
+
session.pendingPatches = new Map();
|
|
846
|
+
}
|
|
847
|
+
session.pendingPatches.set(patchId, patch);
|
|
848
|
+
session.lastActivity = now;
|
|
849
|
+
this._persistSessionState(sessionId);
|
|
850
|
+
return patch;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Mark a pending patch as applied. Moves the record to patchHistory so it
|
|
855
|
+
* is still visible in the summary view but no longer counts as pending.
|
|
856
|
+
*/
|
|
857
|
+
applyPatch(sessionId, patchId, options = {}) {
|
|
858
|
+
const session = this.sessions.get(sessionId);
|
|
859
|
+
if (!session || !(session.pendingPatches instanceof Map)) return null;
|
|
860
|
+
const patch = session.pendingPatches.get(patchId);
|
|
861
|
+
if (!patch) return null;
|
|
862
|
+
|
|
863
|
+
patch.status = "applied";
|
|
864
|
+
patch.resolvedAt = new Date().toISOString();
|
|
865
|
+
patch.resolvedBy = options.resolvedBy || "user";
|
|
866
|
+
if (options.note) {
|
|
867
|
+
patch.note = String(options.note);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
session.pendingPatches.delete(patchId);
|
|
871
|
+
if (!Array.isArray(session.patchHistory)) {
|
|
872
|
+
session.patchHistory = [];
|
|
873
|
+
}
|
|
874
|
+
session.patchHistory.push(patch);
|
|
875
|
+
session.lastActivity = patch.resolvedAt;
|
|
876
|
+
this._persistSessionState(sessionId);
|
|
877
|
+
return patch;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Discard a pending patch. Same bookkeeping as applyPatch but records a
|
|
882
|
+
* "rejected" decision instead.
|
|
883
|
+
*/
|
|
884
|
+
rejectPatch(sessionId, patchId, options = {}) {
|
|
885
|
+
const session = this.sessions.get(sessionId);
|
|
886
|
+
if (!session || !(session.pendingPatches instanceof Map)) return null;
|
|
887
|
+
const patch = session.pendingPatches.get(patchId);
|
|
888
|
+
if (!patch) return null;
|
|
889
|
+
|
|
890
|
+
patch.status = "rejected";
|
|
891
|
+
patch.resolvedAt = new Date().toISOString();
|
|
892
|
+
patch.resolvedBy = options.resolvedBy || "user";
|
|
893
|
+
if (options.reason) {
|
|
894
|
+
patch.rejectionReason = String(options.reason);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
session.pendingPatches.delete(patchId);
|
|
898
|
+
if (!Array.isArray(session.patchHistory)) {
|
|
899
|
+
session.patchHistory = [];
|
|
900
|
+
}
|
|
901
|
+
session.patchHistory.push(patch);
|
|
902
|
+
session.lastActivity = patch.resolvedAt;
|
|
903
|
+
this._persistSessionState(sessionId);
|
|
904
|
+
return patch;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Return a flattened summary of all pending + resolved patches on the
|
|
909
|
+
* session. Shape matches what the renderer strip consumes:
|
|
910
|
+
* { pending: [...], history: [...], totals: { added, removed, fileCount } }
|
|
911
|
+
*/
|
|
912
|
+
getPatchSummary(sessionId) {
|
|
913
|
+
const session = this.sessions.get(sessionId);
|
|
914
|
+
if (!session) return null;
|
|
915
|
+
|
|
916
|
+
const pending =
|
|
917
|
+
session.pendingPatches instanceof Map
|
|
918
|
+
? Array.from(session.pendingPatches.values())
|
|
919
|
+
: [];
|
|
920
|
+
const history = Array.isArray(session.patchHistory)
|
|
921
|
+
? session.patchHistory
|
|
922
|
+
: [];
|
|
923
|
+
|
|
924
|
+
const totals = [...pending, ...history].reduce(
|
|
925
|
+
(acc, patch) => ({
|
|
926
|
+
fileCount: acc.fileCount + (patch.stats?.fileCount || 0),
|
|
927
|
+
added: acc.added + (patch.stats?.added || 0),
|
|
928
|
+
removed: acc.removed + (patch.stats?.removed || 0),
|
|
929
|
+
}),
|
|
930
|
+
{ fileCount: 0, added: 0, removed: 0 },
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
return { pending, history, totals };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
hasPendingPatches(sessionId) {
|
|
937
|
+
const session = this.sessions.get(sessionId);
|
|
938
|
+
if (!session || !(session.pendingPatches instanceof Map)) return false;
|
|
939
|
+
return session.pendingPatches.size > 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
_computePatchStats(file) {
|
|
943
|
+
if (file && file.stats && typeof file.stats === "object") {
|
|
944
|
+
return {
|
|
945
|
+
added: Number(file.stats.added) || 0,
|
|
946
|
+
removed: Number(file.stats.removed) || 0,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const before = file && typeof file.before === "string" ? file.before : "";
|
|
950
|
+
const after = file && typeof file.after === "string" ? file.after : "";
|
|
951
|
+
const beforeLines = before ? before.split(/\r?\n/).length : 0;
|
|
952
|
+
const afterLines = after ? after.split(/\r?\n/).length : 0;
|
|
953
|
+
// Rough heuristic when no explicit diff is provided: full replace counts
|
|
954
|
+
// the entire file as added/removed.
|
|
955
|
+
if (!before && after) return { added: afterLines, removed: 0 };
|
|
956
|
+
if (before && !after) return { added: 0, removed: beforeLines };
|
|
957
|
+
return {
|
|
958
|
+
added: Math.max(0, afterLines - beforeLines),
|
|
959
|
+
removed: Math.max(0, beforeLines - afterLines),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
614
963
|
/**
|
|
615
964
|
* Persist current messages for a session.
|
|
616
965
|
*/
|
|
@@ -689,9 +1038,29 @@ export class WSSessionManager {
|
|
|
689
1038
|
worktreeIsolation: session.worktreeIsolation === true,
|
|
690
1039
|
worktree: session.worktree || null,
|
|
691
1040
|
planSnapshot: this._serializePlanManager(session.planManager),
|
|
1041
|
+
reviewState: session.reviewState || null,
|
|
1042
|
+
pendingPatches:
|
|
1043
|
+
session.pendingPatches instanceof Map
|
|
1044
|
+
? Array.from(session.pendingPatches.values())
|
|
1045
|
+
: [],
|
|
1046
|
+
patchHistory: Array.isArray(session.patchHistory)
|
|
1047
|
+
? session.patchHistory
|
|
1048
|
+
: [],
|
|
692
1049
|
};
|
|
693
1050
|
}
|
|
694
1051
|
|
|
1052
|
+
_hydratePendingPatches(list) {
|
|
1053
|
+
const map = new Map();
|
|
1054
|
+
if (Array.isArray(list)) {
|
|
1055
|
+
for (const patch of list) {
|
|
1056
|
+
if (patch && patch.patchId) {
|
|
1057
|
+
map.set(patch.patchId, patch);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return map;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
695
1064
|
_serializePlanManager(planManager) {
|
|
696
1065
|
if (!planManager) {
|
|
697
1066
|
return null;
|
|
@@ -78,6 +78,36 @@ const CODING_AGENT_EVENT_TYPES = Object.freeze({
|
|
|
78
78
|
SERVER_STARTING: "runtime.server.starting",
|
|
79
79
|
SERVER_READY: "runtime.server.ready",
|
|
80
80
|
SERVER_STOPPED: "runtime.server.stopped",
|
|
81
|
+
|
|
82
|
+
// Sub-agent delegation — lifecycle of child agents spawned from a parent
|
|
83
|
+
// session via the spawn_sub_agent tool. Sequence within a parent requestId
|
|
84
|
+
// stays strictly increasing; parent session + sub-agent id are carried in
|
|
85
|
+
// the payload so UIs can group child events under the parent turn.
|
|
86
|
+
SUB_AGENT_STARTED: "sub-agent.started",
|
|
87
|
+
SUB_AGENT_PROGRESS: "sub-agent.progress",
|
|
88
|
+
SUB_AGENT_COMPLETED: "sub-agent.completed",
|
|
89
|
+
SUB_AGENT_FAILED: "sub-agent.failed",
|
|
90
|
+
SUB_AGENT_LIST: "sub-agent.list",
|
|
91
|
+
|
|
92
|
+
// Review mode — explicit human-in-the-loop (or reviewer sub-agent) gate.
|
|
93
|
+
// When a session enters review mode the runtime MUST block sendMessage
|
|
94
|
+
// until the review is resolved (approved / rejected). Comments can be
|
|
95
|
+
// submitted incrementally by either human reviewers via the UI or by a
|
|
96
|
+
// reviewer role sub-agent writing async findings back to the parent
|
|
97
|
+
// session.
|
|
98
|
+
REVIEW_REQUESTED: "review.requested",
|
|
99
|
+
REVIEW_UPDATED: "review.updated",
|
|
100
|
+
REVIEW_RESOLVED: "review.resolved",
|
|
101
|
+
REVIEW_STATE: "review.state",
|
|
102
|
+
|
|
103
|
+
// Patch preview / diff summary — proposed file edits that the user can
|
|
104
|
+
// preview, approve (apply) or reject before they land on disk. Used to
|
|
105
|
+
// surface a "diff summary" strip in the UI that batches multiple writes
|
|
106
|
+
// from a single turn into a reviewable hunk list.
|
|
107
|
+
PATCH_PROPOSED: "patch.proposed",
|
|
108
|
+
PATCH_APPLIED: "patch.applied",
|
|
109
|
+
PATCH_REJECTED: "patch.rejected",
|
|
110
|
+
PATCH_SUMMARY: "patch.summary",
|
|
81
111
|
});
|
|
82
112
|
|
|
83
113
|
const VALID_TYPE_SET = new Set(Object.values(CODING_AGENT_EVENT_TYPES));
|
|
@@ -330,6 +360,7 @@ const CodingAgentEventType = Object.freeze({
|
|
|
330
360
|
SERVER_STOPPED: CODING_AGENT_EVENT_TYPES.SERVER_STOPPED,
|
|
331
361
|
SESSION_CREATED: CODING_AGENT_EVENT_TYPES.SESSION_STARTED,
|
|
332
362
|
SESSION_RESUMED: CODING_AGENT_EVENT_TYPES.SESSION_RESUMED,
|
|
363
|
+
SESSION_INTERRUPTED: CODING_AGENT_EVENT_TYPES.SESSION_INTERRUPTED,
|
|
333
364
|
SESSION_CLOSED: CODING_AGENT_EVENT_TYPES.SESSION_CLOSED,
|
|
334
365
|
SESSION_LIST: CODING_AGENT_EVENT_TYPES.SESSION_LIST,
|
|
335
366
|
WORKTREE_LIST: CODING_AGENT_EVENT_TYPES.WORKTREE_LIST,
|