poly-weaver 0.14.4 → 0.14.6
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/dist/crosscheck/broadcast-composer.d.ts +12 -7
- package/dist/crosscheck/broadcast-composer.d.ts.map +1 -1
- package/dist/crosscheck/broadcast-composer.js +36 -30
- package/dist/crosscheck/broadcast-composer.js.map +1 -1
- package/dist/crosscheck/coordinator.d.ts +10 -0
- package/dist/crosscheck/coordinator.d.ts.map +1 -1
- package/dist/crosscheck/coordinator.js +247 -56
- package/dist/crosscheck/coordinator.js.map +1 -1
- package/dist/crosscheck/state.d.ts +12 -1
- package/dist/crosscheck/state.d.ts.map +1 -1
- package/dist/crosscheck/state.js +77 -17
- package/dist/crosscheck/state.js.map +1 -1
- package/dist/offscreen/controller.d.ts.map +1 -1
- package/dist/offscreen/controller.js +11 -1
- package/dist/offscreen/controller.js.map +1 -1
- package/dist/offscreen/session-turn-monitor.d.ts +18 -2
- package/dist/offscreen/session-turn-monitor.d.ts.map +1 -1
- package/dist/offscreen/session-turn-monitor.js +156 -18
- package/dist/offscreen/session-turn-monitor.js.map +1 -1
- package/dist/providers/copilot/completion.d.ts +3 -0
- package/dist/providers/copilot/completion.d.ts.map +1 -1
- package/dist/providers/copilot/completion.js +96 -7
- package/dist/providers/copilot/completion.js.map +1 -1
- package/dist/providers/copilot/session.d.ts +10 -8
- package/dist/providers/copilot/session.d.ts.map +1 -1
- package/dist/providers/copilot/session.js +49 -11
- package/dist/providers/copilot/session.js.map +1 -1
- package/dist/providers/copilot/strategy.d.ts.map +1 -1
- package/dist/providers/copilot/strategy.js +5 -1
- package/dist/providers/copilot/strategy.js.map +1 -1
- package/dist/providers/fork.d.ts +25 -0
- package/dist/providers/fork.d.ts.map +1 -1
- package/dist/providers/fork.js.map +1 -1
- package/dist/providers/types.d.ts +4 -2
- package/dist/providers/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { buildExecutorInitialPrompt, buildExecutorCrosscheckPrompt, buildExecuto
|
|
|
6
6
|
import { sendInquiryQuestion } from "../offscreen/inquiry-sender.js";
|
|
7
7
|
import { SessionTurnMonitor } from "../offscreen/session-turn-monitor.js";
|
|
8
8
|
import { resolveProvider } from "../providers/registry.js";
|
|
9
|
+
import { readSessionFileWithStatus } from "../session/reader.js";
|
|
9
10
|
import { encodeKeysToBytes } from "../terminal/key-to-bytes.js";
|
|
10
11
|
import { WHEEL_LINES_PER_EVENT, } from "../terminal/input-router.js";
|
|
11
12
|
import { isUnmodifiedRightButtonGestureFrame, isUnmodifiedRightButtonPress, isUnmodifiedRightButtonRelease, parseSgrMouseFrame, } from "../terminal/sgr-mouse.js";
|
|
@@ -73,6 +74,8 @@ export class CrosscheckCoordinator {
|
|
|
73
74
|
dispatching = false;
|
|
74
75
|
broadcastSendActive = false;
|
|
75
76
|
dispatchAbort = new AbortController();
|
|
77
|
+
operationFence = 0;
|
|
78
|
+
activeOperationAborts = new Set();
|
|
76
79
|
dispatchOperations = new Set();
|
|
77
80
|
activityOperations = new Set();
|
|
78
81
|
resendAfterLaunch = new Set();
|
|
@@ -105,6 +108,7 @@ export class CrosscheckCoordinator {
|
|
|
105
108
|
this.detachUi = this.options.host.attachParentUi(this);
|
|
106
109
|
const layout = this.options.host.getLayout();
|
|
107
110
|
const dimensions = this.paneDimensions(layout.columns, layout.childRows);
|
|
111
|
+
await this.reconcilePersistedHistory();
|
|
108
112
|
const persisted = this.store.snapshot();
|
|
109
113
|
const launchAbort = new AbortController();
|
|
110
114
|
this.launchAbort = launchAbort;
|
|
@@ -120,7 +124,8 @@ export class CrosscheckCoordinator {
|
|
|
120
124
|
try {
|
|
121
125
|
const config = this.options.configurations[agent];
|
|
122
126
|
const resultFile = persisted.generation.resultFiles[agent];
|
|
123
|
-
const
|
|
127
|
+
const resultState = persisted.generation.rewind?.[agent];
|
|
128
|
+
const resultReady = resultState ? false : await fileHasContent(resultFile);
|
|
124
129
|
const prior = persisted.sessions[agent];
|
|
125
130
|
const resumeIdentity = prior?.driver === config.driver
|
|
126
131
|
? {
|
|
@@ -145,13 +150,15 @@ export class CrosscheckCoordinator {
|
|
|
145
150
|
: resumeIdentity && priorSubmitCount !== undefined
|
|
146
151
|
? await resolveProvider(config.driver).live.classifyGenerationDelivery(prior.sessionFilePath, priorSubmitCount) === "present"
|
|
147
152
|
: undefined;
|
|
148
|
-
const prompt =
|
|
149
|
-
?
|
|
150
|
-
:
|
|
151
|
-
?
|
|
152
|
-
: activityState === "
|
|
153
|
-
?
|
|
154
|
-
:
|
|
153
|
+
const prompt = persisted.generation.rewind
|
|
154
|
+
? ""
|
|
155
|
+
: generationDelivered === false
|
|
156
|
+
? resumePrompt
|
|
157
|
+
: activityState === "idle"
|
|
158
|
+
? ""
|
|
159
|
+
: activityState === "working"
|
|
160
|
+
? RESUME_CONTINUE_PROMPT
|
|
161
|
+
: resumePrompt;
|
|
155
162
|
const session = await (this.options.launchSession ?? LiveAgentSession.launch)({
|
|
156
163
|
driver: config.driver,
|
|
157
164
|
workdir: this.options.workdir,
|
|
@@ -180,6 +187,13 @@ export class CrosscheckCoordinator {
|
|
|
180
187
|
});
|
|
181
188
|
this.launchingSessions.delete(agent);
|
|
182
189
|
const monitor = new SessionTurnMonitor(config.driver, session.identity.sessionFilePath, session.baseline, {
|
|
190
|
+
initialHistorySnapshot: persisted.sessions[agent]?.historySnapshot,
|
|
191
|
+
onHistorySnapshot: (snapshot) => {
|
|
192
|
+
void this.store.updateSessionHistory(agent, snapshot);
|
|
193
|
+
},
|
|
194
|
+
onHistoryRegression: (_regression, snapshot) => {
|
|
195
|
+
this.runDispatchOperation(agent, this.invalidateGenerationForRewind(agent, snapshot), "invalidate rewound generation");
|
|
196
|
+
},
|
|
183
197
|
onChange: (idle) => {
|
|
184
198
|
this.refreshStatusHint();
|
|
185
199
|
this.options.host.scheduleParentUiRender();
|
|
@@ -204,9 +218,11 @@ export class CrosscheckCoordinator {
|
|
|
204
218
|
session,
|
|
205
219
|
monitor,
|
|
206
220
|
resultReady,
|
|
221
|
+
resultState,
|
|
207
222
|
freshForPendingDispatch,
|
|
208
223
|
launchTurnExpected: session.resumed !== true || prompt.length > 0,
|
|
209
224
|
});
|
|
225
|
+
monitor.startHistoryPolling();
|
|
210
226
|
await this.store.update((state) => {
|
|
211
227
|
const dispatch = state.generation.dispatch;
|
|
212
228
|
if (freshForPendingDispatch
|
|
@@ -225,6 +241,9 @@ export class CrosscheckCoordinator {
|
|
|
225
241
|
...(freshForPendingDispatch && {
|
|
226
242
|
pendingDispatchBaselineReset: true,
|
|
227
243
|
}),
|
|
244
|
+
...(persisted.sessions[agent]?.historySnapshot && {
|
|
245
|
+
historySnapshot: persisted.sessions[agent].historySnapshot,
|
|
246
|
+
}),
|
|
228
247
|
};
|
|
229
248
|
});
|
|
230
249
|
if (freshForPendingDispatch
|
|
@@ -261,7 +280,7 @@ export class CrosscheckCoordinator {
|
|
|
261
280
|
}
|
|
262
281
|
await this.reconcilePersistedDispatch();
|
|
263
282
|
await this.maybeSettleReconcileGate();
|
|
264
|
-
this.broadcast.reconcileMode(this.currentReadiness());
|
|
283
|
+
this.broadcast.reconcileMode(this.currentReadiness(), this.currentGenerationPresentation());
|
|
265
284
|
this.refreshMouseModes();
|
|
266
285
|
this.resultTimer = setInterval(() => {
|
|
267
286
|
void this.refreshResults();
|
|
@@ -298,7 +317,7 @@ export class CrosscheckCoordinator {
|
|
|
298
317
|
focusSwitchHint: this.focusSwitchHint(),
|
|
299
318
|
version: `poly-weaver v${APP_VERSION}`,
|
|
300
319
|
transient: this.currentTransientFlash(),
|
|
301
|
-
});
|
|
320
|
+
}, this.currentGenerationPresentation());
|
|
302
321
|
const lines = [...top.lines.slice(0, agentAreaRows), ...broadcastPane.lines];
|
|
303
322
|
while (lines.length < totalRows)
|
|
304
323
|
lines.push("");
|
|
@@ -548,7 +567,7 @@ export class CrosscheckCoordinator {
|
|
|
548
567
|
if (this.focus === "broadcast") {
|
|
549
568
|
flush();
|
|
550
569
|
const submissionState = this.currentSubmissionState();
|
|
551
|
-
const outcome = this.broadcast.handleKey(event, this.currentReadiness(), submissionState);
|
|
570
|
+
const outcome = this.broadcast.handleKey(event, this.currentReadiness(), submissionState, 0, this.currentGenerationPresentation());
|
|
552
571
|
if (outcome === "submit") {
|
|
553
572
|
this.runDispatchOperation(undefined, this.submitBroadcast(), "broadcast send");
|
|
554
573
|
}
|
|
@@ -608,12 +627,14 @@ export class CrosscheckCoordinator {
|
|
|
608
627
|
const focused = this.focus === agent;
|
|
609
628
|
const config = runtime?.config ?? this.options.configurations[agent];
|
|
610
629
|
const monitor = runtime?.monitor;
|
|
630
|
+
const resultSlot = runtime?.resultState
|
|
631
|
+
?? (runtime?.resultReady ? "done" : undefined);
|
|
611
632
|
const label = [
|
|
612
633
|
config.driver,
|
|
613
634
|
config.model,
|
|
614
635
|
config.effort,
|
|
615
636
|
monitor ? (monitor.isIdle ? "idle" : "working") : "starting",
|
|
616
|
-
|
|
637
|
+
resultSlot,
|
|
617
638
|
].filter(Boolean).join(" | ");
|
|
618
639
|
const color = focused
|
|
619
640
|
? `${MUTED_YELLOW_BG}${TRUE_BLACK_FG}${bold}`
|
|
@@ -770,6 +791,11 @@ export class CrosscheckCoordinator {
|
|
|
770
791
|
agent2Ready: !!this.agents.get("agent-2")?.resultReady,
|
|
771
792
|
};
|
|
772
793
|
}
|
|
794
|
+
currentGenerationPresentation() {
|
|
795
|
+
return this.store?.generationInvalidated()
|
|
796
|
+
? { kind: "rewound" }
|
|
797
|
+
: { kind: "active" };
|
|
798
|
+
}
|
|
773
799
|
currentResultFile(agent) {
|
|
774
800
|
return this.store.snapshot().generation.resultFiles[agent];
|
|
775
801
|
}
|
|
@@ -779,8 +805,63 @@ export class CrosscheckCoordinator {
|
|
|
779
805
|
const generation = this.store.snapshot().generation;
|
|
780
806
|
return generation.id === 0 ? this.options.task : generation.prompt;
|
|
781
807
|
}
|
|
808
|
+
async reconcilePersistedHistory() {
|
|
809
|
+
const snapshot = this.store.snapshot();
|
|
810
|
+
await Promise.all(["agent-1", "agent-2"].map(async (agent) => {
|
|
811
|
+
const session = snapshot.sessions[agent];
|
|
812
|
+
const config = this.options.configurations[agent];
|
|
813
|
+
if (!session || session.driver !== config.driver || !session.historySnapshot) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const provider = resolveProvider(config.driver);
|
|
817
|
+
const buildSnapshot = provider.fork.sessionHistorySnapshot;
|
|
818
|
+
const compare = provider.fork.compareSessionHistory;
|
|
819
|
+
if (!buildSnapshot || !compare)
|
|
820
|
+
return;
|
|
821
|
+
try {
|
|
822
|
+
const { lines, malformed } = await readSessionFileWithStatus(session.sessionFilePath);
|
|
823
|
+
if (malformed)
|
|
824
|
+
return;
|
|
825
|
+
const current = buildSnapshot(lines);
|
|
826
|
+
if (!current)
|
|
827
|
+
return;
|
|
828
|
+
if (compare(session.historySnapshot, current)) {
|
|
829
|
+
await this.invalidateGenerationForRewind(agent, current);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
await this.store.updateSessionHistory(agent, current);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// Unreadable history is indeterminate; it is never proof of rewind.
|
|
837
|
+
}
|
|
838
|
+
}));
|
|
839
|
+
}
|
|
840
|
+
async invalidateGenerationForRewind(agent, snapshot) {
|
|
841
|
+
this.abortActiveDispatchOperations(new Error("Crosscheck generation invalidated by session rewind"));
|
|
842
|
+
await this.store.invalidateGenerationForRewind(agent, snapshot);
|
|
843
|
+
const rewind = this.store.generationRewindState();
|
|
844
|
+
for (const side of ["agent-1", "agent-2"]) {
|
|
845
|
+
const runtime = this.agents.get(side);
|
|
846
|
+
if (!runtime)
|
|
847
|
+
continue;
|
|
848
|
+
runtime.resultReady = false;
|
|
849
|
+
runtime.resultState = rewind?.[side];
|
|
850
|
+
runtime.freshForPendingDispatch = false;
|
|
851
|
+
}
|
|
852
|
+
this.crosscheckProgress = undefined;
|
|
853
|
+
this.crosscheckTurnIdentities.clear();
|
|
854
|
+
this.resendAfterLaunch.clear();
|
|
855
|
+
this.reconciliationBlocked.clear();
|
|
856
|
+
this.freshDispatchPreparations.clear();
|
|
857
|
+
this.broadcast.reconcileMode(this.currentReadiness(), this.currentGenerationPresentation());
|
|
858
|
+
this.refreshStatusHint();
|
|
859
|
+
this.options.host.scheduleParentUiRender();
|
|
860
|
+
}
|
|
782
861
|
currentCrosscheckStatus() {
|
|
783
862
|
const progress = this.crosscheckProgress;
|
|
863
|
+
if (this.store?.generationInvalidated())
|
|
864
|
+
return { kind: "unavailable" };
|
|
784
865
|
if (progress
|
|
785
866
|
&& this.store
|
|
786
867
|
&& this.store.generationId() === progress.generationId) {
|
|
@@ -831,6 +912,8 @@ export class CrosscheckCoordinator {
|
|
|
831
912
|
isCrosscheckArmed() {
|
|
832
913
|
if (!this.store || !this.sessionsReady)
|
|
833
914
|
return false;
|
|
915
|
+
if (this.store.generationInvalidated())
|
|
916
|
+
return false;
|
|
834
917
|
if (this.dispatching || this.store.generationConsumed() || this.store.hasDispatch()) {
|
|
835
918
|
return false;
|
|
836
919
|
}
|
|
@@ -839,6 +922,8 @@ export class CrosscheckCoordinator {
|
|
|
839
922
|
isReconcileArmed() {
|
|
840
923
|
if (!this.store || !this.sessionsReady)
|
|
841
924
|
return false;
|
|
925
|
+
if (this.store.generationInvalidated())
|
|
926
|
+
return false;
|
|
842
927
|
if (this.dispatching
|
|
843
928
|
|| !this.store.generationConsumed()
|
|
844
929
|
|| this.store.hasDispatch()
|
|
@@ -976,9 +1061,11 @@ export class CrosscheckCoordinator {
|
|
|
976
1061
|
const attemptId = this.beginCrosscheckProgress(generationId, token, phase);
|
|
977
1062
|
const agents = ["agent-1", "agent-2"];
|
|
978
1063
|
try {
|
|
979
|
-
await this.withDispatchLifecycle(agents, async () => {
|
|
1064
|
+
await this.withDispatchLifecycle(agents, async (operation) => {
|
|
980
1065
|
await this.store.update((state) => {
|
|
981
|
-
if (state.generation.id !== generationId
|
|
1066
|
+
if (state.generation.id !== generationId
|
|
1067
|
+
|| state.generation.rewind
|
|
1068
|
+
|| !this.isOperationCurrent(operation))
|
|
982
1069
|
return;
|
|
983
1070
|
state.generation.reconcileAllowed = false;
|
|
984
1071
|
state.generation.reconcileSettled = false;
|
|
@@ -998,12 +1085,16 @@ export class CrosscheckCoordinator {
|
|
|
998
1085
|
},
|
|
999
1086
|
};
|
|
1000
1087
|
});
|
|
1001
|
-
|
|
1088
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1089
|
+
return;
|
|
1090
|
+
const results = await Promise.allSettled(agents.map((agent) => this.sendDispatchAgent(agent, generationId, attemptId, operation)));
|
|
1002
1091
|
for (const [index, result] of results.entries()) {
|
|
1003
1092
|
if (result.status === "rejected") {
|
|
1004
1093
|
this.warnDispatchFailure(agents[index], "send", result.reason);
|
|
1005
1094
|
}
|
|
1006
1095
|
}
|
|
1096
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1097
|
+
return;
|
|
1007
1098
|
await this.finalizeDispatchIfComplete(generationId);
|
|
1008
1099
|
});
|
|
1009
1100
|
}
|
|
@@ -1011,12 +1102,12 @@ export class CrosscheckCoordinator {
|
|
|
1011
1102
|
this.settleUnsubmittedCrosscheckSlots(attemptId);
|
|
1012
1103
|
}
|
|
1013
1104
|
}
|
|
1014
|
-
async sendDispatchAgent(agent, generationId, progressAttemptId) {
|
|
1105
|
+
async sendDispatchAgent(agent, generationId, progressAttemptId, operation = this.currentDispatchOperationContext()) {
|
|
1015
1106
|
if (this.reconciliationBlocked.has(agent))
|
|
1016
1107
|
return;
|
|
1017
|
-
|
|
1018
|
-
if (state.generation.id !== generationId)
|
|
1108
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1019
1109
|
return;
|
|
1110
|
+
const state = this.store.snapshot();
|
|
1020
1111
|
const dispatch = state.generation.dispatch;
|
|
1021
1112
|
if (!dispatch || dispatch.agents[agent].status !== "pending")
|
|
1022
1113
|
return;
|
|
@@ -1028,9 +1119,11 @@ export class CrosscheckCoordinator {
|
|
|
1028
1119
|
const releaseInputLock = this.lockAutomatedInput(agent);
|
|
1029
1120
|
try {
|
|
1030
1121
|
await (this.options.sendInquiry ?? sendInquiryQuestion)(runtime.session, this.buildDispatchPrompt(agent, generationId, dispatch.phase, dispatch.round), {
|
|
1031
|
-
signal:
|
|
1122
|
+
signal: operation.signal,
|
|
1032
1123
|
composer: provider.composer,
|
|
1033
1124
|
onSubmitted: () => {
|
|
1125
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1126
|
+
return;
|
|
1034
1127
|
submitted = true;
|
|
1035
1128
|
if (progressAttemptId) {
|
|
1036
1129
|
this.markCrosscheckSlotSubmitted(agent, generationId, dispatch.token, progressAttemptId);
|
|
@@ -1042,13 +1135,17 @@ export class CrosscheckCoordinator {
|
|
|
1042
1135
|
finally {
|
|
1043
1136
|
releaseInputLock();
|
|
1044
1137
|
}
|
|
1045
|
-
if (!submitted)
|
|
1138
|
+
if (!submitted || !this.isCurrentGenerationOperation(generationId, operation))
|
|
1046
1139
|
return;
|
|
1047
1140
|
await this.transitionDispatchStatus(agent, generationId, dispatch.token, "pending", "submitted");
|
|
1048
1141
|
const classification = await provider.live.classifyDispatchDelivery(runtime.session.identity.sessionFilePath, dispatch.agents[agent].baseline);
|
|
1142
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1143
|
+
return;
|
|
1049
1144
|
if (classification === "present") {
|
|
1050
1145
|
await this.markDispatchDelivered(agent, generationId, dispatch.token, "submitted");
|
|
1051
1146
|
}
|
|
1147
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1148
|
+
return;
|
|
1052
1149
|
await this.finalizeDispatchIfComplete(generationId);
|
|
1053
1150
|
}
|
|
1054
1151
|
buildDispatchPrompt(agent, generationId, phase, round) {
|
|
@@ -1098,9 +1195,9 @@ export class CrosscheckCoordinator {
|
|
|
1098
1195
|
const lockedAgents = ["agent-1", "agent-2"].filter((agent) => stateAtStart.generation.dispatch?.agents[agent].status === "pending"
|
|
1099
1196
|
&& !this.reconciliationBlocked.has(agent));
|
|
1100
1197
|
try {
|
|
1101
|
-
await this.withDispatchLifecycle(lockedAgents, async () => {
|
|
1198
|
+
await this.withDispatchLifecycle(lockedAgents, async (operation) => {
|
|
1102
1199
|
const state = this.store.snapshot();
|
|
1103
|
-
if (
|
|
1200
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1104
1201
|
return;
|
|
1105
1202
|
const dispatch = state.generation.dispatch;
|
|
1106
1203
|
if (!dispatch || (state.generation.consumed && dispatch.phase === "crosscheck")) {
|
|
@@ -1114,15 +1211,17 @@ export class CrosscheckCoordinator {
|
|
|
1114
1211
|
continue;
|
|
1115
1212
|
const current = await this.reconcileDispatchSide(agent, generationId, dispatch, expectedStatus, async () => {
|
|
1116
1213
|
try {
|
|
1117
|
-
await this.sendDispatchAgent(agent, generationId, attemptId);
|
|
1214
|
+
await this.sendDispatchAgent(agent, generationId, attemptId, operation);
|
|
1118
1215
|
}
|
|
1119
1216
|
catch (err) {
|
|
1120
1217
|
this.warnDispatchFailure(agent, "retry", err);
|
|
1121
1218
|
}
|
|
1122
|
-
});
|
|
1219
|
+
}, operation);
|
|
1123
1220
|
if (!current)
|
|
1124
1221
|
return;
|
|
1125
1222
|
}
|
|
1223
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1224
|
+
return;
|
|
1126
1225
|
await this.finalizeDispatchIfComplete(generationId);
|
|
1127
1226
|
});
|
|
1128
1227
|
}
|
|
@@ -1130,7 +1229,9 @@ export class CrosscheckCoordinator {
|
|
|
1130
1229
|
this.settleUnsubmittedCrosscheckSlots(attemptId);
|
|
1131
1230
|
}
|
|
1132
1231
|
}
|
|
1133
|
-
async reconcileDispatchSide(agent, generationId, dispatch, expectedStatus, onAbsent) {
|
|
1232
|
+
async reconcileDispatchSide(agent, generationId, dispatch, expectedStatus, onAbsent, operation = this.currentDispatchOperationContext()) {
|
|
1233
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1234
|
+
return false;
|
|
1134
1235
|
const target = onAbsent
|
|
1135
1236
|
? (() => {
|
|
1136
1237
|
const runtime = this.agents.get(agent);
|
|
@@ -1155,6 +1256,8 @@ export class CrosscheckCoordinator {
|
|
|
1155
1256
|
}
|
|
1156
1257
|
const classification = await resolveProvider(target.driver)
|
|
1157
1258
|
.live.classifyDispatchDelivery(target.sessionFilePath, dispatch.agents[agent].baseline);
|
|
1259
|
+
if (!this.isCurrentGenerationOperation(generationId, operation))
|
|
1260
|
+
return false;
|
|
1158
1261
|
if (!this.isCurrentDispatch(generationId, dispatch.token))
|
|
1159
1262
|
return false;
|
|
1160
1263
|
if (!this.isCurrentDispatchSide(agent, generationId, dispatch.token, expectedStatus))
|
|
@@ -1185,14 +1288,16 @@ export class CrosscheckCoordinator {
|
|
|
1185
1288
|
return true;
|
|
1186
1289
|
}
|
|
1187
1290
|
async withDispatchLifecycle(lockedAgents, dispatch) {
|
|
1291
|
+
const operation = this.beginDispatchOperation();
|
|
1188
1292
|
this.dispatching = true;
|
|
1189
1293
|
this.refreshStatusHint();
|
|
1190
1294
|
this.options.host.scheduleParentUiRender();
|
|
1191
1295
|
const releaseInputLocks = lockedAgents.map((agent) => this.lockAutomatedInput(agent));
|
|
1192
1296
|
try {
|
|
1193
|
-
await dispatch();
|
|
1297
|
+
await dispatch(operation);
|
|
1194
1298
|
}
|
|
1195
1299
|
finally {
|
|
1300
|
+
operation.release();
|
|
1196
1301
|
for (const release of releaseInputLocks)
|
|
1197
1302
|
release();
|
|
1198
1303
|
this.dispatching = false;
|
|
@@ -1235,28 +1340,40 @@ export class CrosscheckCoordinator {
|
|
|
1235
1340
|
}
|
|
1236
1341
|
async settleReconcileGate(generationId) {
|
|
1237
1342
|
const generation = this.store.snapshot().generation;
|
|
1343
|
+
const round = generation.reconcileRound;
|
|
1238
1344
|
if (generation.id !== generationId
|
|
1239
1345
|
|| !generation.reconcilePendingSettlement
|
|
1240
|
-
|| generation.dispatch
|
|
1346
|
+
|| generation.dispatch
|
|
1347
|
+
|| !this.allAgentsIdle())
|
|
1241
1348
|
return;
|
|
1242
|
-
const round = generation.reconcileRound;
|
|
1243
1349
|
const paths = ["agent-1", "agent-2"].map((agent) => round === 0
|
|
1244
1350
|
? crosscheckJsonFilePath(this.options.persistentDir, agent, generationId)
|
|
1245
1351
|
: reconcileJsonFilePath(this.options.persistentDir, agent, generationId, round));
|
|
1246
1352
|
const [agent1, agent2] = await Promise.all(paths.map((path) => readReviewResult(path)));
|
|
1247
|
-
|
|
1353
|
+
const afterRead = this.store.snapshot().generation;
|
|
1354
|
+
if (afterRead.id !== generationId
|
|
1355
|
+
|| afterRead.reconcileRound !== round
|
|
1356
|
+
|| !afterRead.reconcilePendingSettlement
|
|
1357
|
+
|| afterRead.dispatch
|
|
1358
|
+
|| !this.allAgentsIdle())
|
|
1248
1359
|
return;
|
|
1249
1360
|
if (!agent1 || !agent2) {
|
|
1361
|
+
let markedMissing = false;
|
|
1250
1362
|
await this.store.update((state) => {
|
|
1251
1363
|
if (state.generation.id !== generationId
|
|
1364
|
+
|| state.generation.reconcileRound !== round
|
|
1252
1365
|
|| !state.generation.reconcilePendingSettlement
|
|
1253
|
-
|| state.generation.dispatch
|
|
1366
|
+
|| state.generation.dispatch
|
|
1367
|
+
|| !this.allAgentsIdle())
|
|
1254
1368
|
return;
|
|
1255
1369
|
state.generation.reconcileJsonMissing = true;
|
|
1256
1370
|
state.generation.reconcileAllowed = false;
|
|
1257
1371
|
state.generation.reconcileSettled = false;
|
|
1258
1372
|
state.generation.reconcilePendingSettlement = false;
|
|
1373
|
+
markedMissing = true;
|
|
1259
1374
|
});
|
|
1375
|
+
if (!markedMissing)
|
|
1376
|
+
return;
|
|
1260
1377
|
this.options.output.warn("Crosscheck review JSON is missing or malformed; Ctrl+X Reconcile is unavailable.");
|
|
1261
1378
|
this.refreshStatusHint();
|
|
1262
1379
|
this.options.host.scheduleParentUiRender();
|
|
@@ -1265,16 +1382,22 @@ export class CrosscheckCoordinator {
|
|
|
1265
1382
|
const reconcileAllowed = round === 0
|
|
1266
1383
|
? true
|
|
1267
1384
|
: agent1.rejected.length > 0 || agent2.rejected.length > 0;
|
|
1385
|
+
let settled = false;
|
|
1268
1386
|
await this.store.update((state) => {
|
|
1269
1387
|
if (state.generation.id !== generationId
|
|
1388
|
+
|| state.generation.reconcileRound !== round
|
|
1270
1389
|
|| !state.generation.reconcilePendingSettlement
|
|
1271
|
-
|| state.generation.dispatch
|
|
1390
|
+
|| state.generation.dispatch
|
|
1391
|
+
|| !this.allAgentsIdle())
|
|
1272
1392
|
return;
|
|
1273
1393
|
state.generation.reconcileJsonMissing = false;
|
|
1274
1394
|
state.generation.reconcileAllowed = reconcileAllowed;
|
|
1275
1395
|
state.generation.reconcileSettled = true;
|
|
1276
1396
|
state.generation.reconcilePendingSettlement = false;
|
|
1397
|
+
settled = true;
|
|
1277
1398
|
});
|
|
1399
|
+
if (!settled)
|
|
1400
|
+
return;
|
|
1278
1401
|
this.refreshStatusHint();
|
|
1279
1402
|
this.options.host.scheduleParentUiRender();
|
|
1280
1403
|
}
|
|
@@ -1287,6 +1410,7 @@ export class CrosscheckCoordinator {
|
|
|
1287
1410
|
transitionDispatchStatus(agent, generationId, dispatchToken, expectedStatus, nextStatus) {
|
|
1288
1411
|
return this.store.update((state) => {
|
|
1289
1412
|
if (state.generation.id === generationId
|
|
1413
|
+
&& !state.generation.rewind
|
|
1290
1414
|
&& state.generation.dispatch?.token === dispatchToken
|
|
1291
1415
|
&& state.generation.dispatch.agents[agent].status === expectedStatus) {
|
|
1292
1416
|
state.generation.dispatch.agents[agent].status = nextStatus;
|
|
@@ -1321,30 +1445,35 @@ export class CrosscheckCoordinator {
|
|
|
1321
1445
|
this.refreshStatusHint();
|
|
1322
1446
|
this.options.host.scheduleParentUiRender();
|
|
1323
1447
|
try {
|
|
1324
|
-
|
|
1325
|
-
try {
|
|
1326
|
-
finalizedText = await finalizeImageAttachments(text, this.broadcast.attachments, this.options.persistentDir);
|
|
1327
|
-
}
|
|
1328
|
-
catch (err) {
|
|
1329
|
-
this.flashTransient(err instanceof Error ? err.message : String(err), 3000);
|
|
1330
|
-
throw err;
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
else {
|
|
1334
|
-
finalizedText = text;
|
|
1335
|
-
}
|
|
1336
|
-
await this.withDispatchLifecycle(agents, async () => {
|
|
1448
|
+
await this.withDispatchLifecycle(agents, async (operation) => {
|
|
1337
1449
|
for (const agent of agents) {
|
|
1338
1450
|
if (!this.agents.has(agent)) {
|
|
1339
1451
|
throw new Error(`${agentLabel(agent)} is not ready for Broadcast`);
|
|
1340
1452
|
}
|
|
1341
1453
|
}
|
|
1454
|
+
if (hasImagePlaceholders(text)) {
|
|
1455
|
+
try {
|
|
1456
|
+
finalizedText = await finalizeImageAttachments(text, this.broadcast.attachments, this.options.persistentDir);
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
this.flashTransient(err instanceof Error ? err.message : String(err), 3000);
|
|
1460
|
+
throw err;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
finalizedText = text;
|
|
1465
|
+
}
|
|
1466
|
+
if (!this.isOperationCurrent(operation))
|
|
1467
|
+
return;
|
|
1342
1468
|
if (mode === "steer" || mode === "follow-up") {
|
|
1343
1469
|
const deliverySubmitCounts = await this.captureGenerationDeliveryCounts();
|
|
1470
|
+
if (!this.isOperationCurrent(operation))
|
|
1471
|
+
return;
|
|
1344
1472
|
await this.store.startGeneration(mode, finalizedText, deliverySubmitCounts);
|
|
1345
1473
|
this.crosscheckProgress = undefined;
|
|
1346
1474
|
for (const runtime of this.agents.values()) {
|
|
1347
1475
|
runtime.resultReady = false;
|
|
1476
|
+
runtime.resultState = undefined;
|
|
1348
1477
|
runtime.freshForPendingDispatch = false;
|
|
1349
1478
|
}
|
|
1350
1479
|
this.resendAfterLaunch.clear();
|
|
@@ -1356,12 +1485,14 @@ export class CrosscheckCoordinator {
|
|
|
1356
1485
|
const generationKind = mode === "steer" || mode === "follow-up"
|
|
1357
1486
|
? mode
|
|
1358
1487
|
: undefined;
|
|
1359
|
-
const sends = agents.map((agent) => this.sendBroadcastAgent(agent, finalizedText, generationId, generationKind));
|
|
1488
|
+
const sends = agents.map((agent) => this.sendBroadcastAgent(agent, finalizedText, generationId, generationKind, operation));
|
|
1360
1489
|
initiated = true;
|
|
1361
1490
|
this.broadcast.completeSubmission(submission);
|
|
1362
1491
|
this.resizeSessions();
|
|
1363
1492
|
this.options.host.scheduleParentUiRender();
|
|
1364
1493
|
const results = await Promise.allSettled(sends);
|
|
1494
|
+
if (!this.isOperationCurrent(operation))
|
|
1495
|
+
return;
|
|
1365
1496
|
for (const [index, result] of results.entries()) {
|
|
1366
1497
|
if (result.status === "rejected") {
|
|
1367
1498
|
this.warnDispatchFailure(agents[index], "broadcast send", result.reason);
|
|
@@ -1385,19 +1516,18 @@ export class CrosscheckCoordinator {
|
|
|
1385
1516
|
return [agent, runtime];
|
|
1386
1517
|
});
|
|
1387
1518
|
await Promise.all(runtimes.map(([, runtime]) => runtime.monitor.waitForPendingTurnAttempts()));
|
|
1388
|
-
const entries = await Promise.all(runtimes.map(async ([agent, runtime]) =>
|
|
1389
|
-
const lines = await resolveProvider(runtime.config.driver).live.readLines(runtime.session.identity.sessionFilePath);
|
|
1390
|
-
return [agent, runtime.monitor.expectedSubmitCount(lines)];
|
|
1391
|
-
}));
|
|
1519
|
+
const entries = await Promise.all(runtimes.map(async ([agent, runtime]) => [agent, await runtime.monitor.expectedSubmitCountFromSession()]));
|
|
1392
1520
|
return Object.fromEntries(entries);
|
|
1393
1521
|
}
|
|
1394
|
-
async sendBroadcastAgent(agent, text, generationId, generationKind) {
|
|
1522
|
+
async sendBroadcastAgent(agent, text, generationId, generationKind, operation = this.currentDispatchOperationContext()) {
|
|
1395
1523
|
const runtime = this.agents.get(agent);
|
|
1396
1524
|
if (!runtime)
|
|
1397
1525
|
throw new Error(`${agentLabel(agent)} is not ready for Broadcast`);
|
|
1398
|
-
const
|
|
1399
|
-
if (
|
|
1526
|
+
const conversational = generationKind === undefined;
|
|
1527
|
+
if (!this.isCurrentGenerationOperation(generationId, operation, { allowRewound: conversational })) {
|
|
1400
1528
|
return;
|
|
1529
|
+
}
|
|
1530
|
+
const generation = this.store.snapshot().generation;
|
|
1401
1531
|
const prompt = generationKind === "steer"
|
|
1402
1532
|
? buildExecutorSteerPrompt(text, generation.resultFiles[agent])
|
|
1403
1533
|
: generationKind === "follow-up"
|
|
@@ -1407,12 +1537,19 @@ export class CrosscheckCoordinator {
|
|
|
1407
1537
|
const releaseInputLock = this.lockAutomatedInput(agent);
|
|
1408
1538
|
try {
|
|
1409
1539
|
if (generationKind && provider.cancelSequence) {
|
|
1410
|
-
await sendCancelSequence(runtime.session.pty, provider.cancelSequence,
|
|
1540
|
+
await sendCancelSequence(runtime.session.pty, provider.cancelSequence, operation.signal, runtime.monitor.isIdle);
|
|
1541
|
+
}
|
|
1542
|
+
if (!this.isCurrentGenerationOperation(generationId, operation, { allowRewound: conversational })) {
|
|
1543
|
+
return;
|
|
1411
1544
|
}
|
|
1412
1545
|
await (this.options.sendInquiry ?? sendInquiryQuestion)(runtime.session, prompt, {
|
|
1413
|
-
signal:
|
|
1546
|
+
signal: operation.signal,
|
|
1414
1547
|
composer: provider.composer,
|
|
1415
|
-
onSubmitted: () =>
|
|
1548
|
+
onSubmitted: () => {
|
|
1549
|
+
if (!this.isCurrentGenerationOperation(generationId, operation, { allowRewound: conversational }))
|
|
1550
|
+
return;
|
|
1551
|
+
runtime.monitor.beginTurn();
|
|
1552
|
+
},
|
|
1416
1553
|
...(generationKind ? { escapeBeforePaste: true } : {}),
|
|
1417
1554
|
});
|
|
1418
1555
|
}
|
|
@@ -1433,6 +1570,8 @@ export class CrosscheckCoordinator {
|
|
|
1433
1570
|
async performResultRefresh() {
|
|
1434
1571
|
let changed = false;
|
|
1435
1572
|
const generation = this.store.snapshot().generation;
|
|
1573
|
+
if (generation.rewind)
|
|
1574
|
+
return;
|
|
1436
1575
|
const readiness = await Promise.all(["agent-1", "agent-2"].map(async (agent) => ({
|
|
1437
1576
|
agent,
|
|
1438
1577
|
ready: await fileHasContent(generation.resultFiles[agent]),
|
|
@@ -1448,7 +1587,7 @@ export class CrosscheckCoordinator {
|
|
|
1448
1587
|
}
|
|
1449
1588
|
}
|
|
1450
1589
|
if (changed) {
|
|
1451
|
-
this.broadcast.reconcileMode(this.currentReadiness());
|
|
1590
|
+
this.broadcast.reconcileMode(this.currentReadiness(), this.currentGenerationPresentation());
|
|
1452
1591
|
this.refreshStatusHint();
|
|
1453
1592
|
this.options.host.scheduleParentUiRender();
|
|
1454
1593
|
this.maybeRetryPersistedDispatch();
|
|
@@ -1630,6 +1769,7 @@ export class CrosscheckCoordinator {
|
|
|
1630
1769
|
this.finishing = true;
|
|
1631
1770
|
this.launchAbort?.abort(new Error("Crosscheck launch cancelled"));
|
|
1632
1771
|
this.dispatchAbort.abort(new Error("Crosscheck coordinator stopped"));
|
|
1772
|
+
this.abortActiveDispatchOperations(new Error("Crosscheck coordinator stopped"));
|
|
1633
1773
|
this.finishResolve?.({ kind: "crosscheck", interrupted: true });
|
|
1634
1774
|
}
|
|
1635
1775
|
fail(error) {
|
|
@@ -1638,6 +1778,7 @@ export class CrosscheckCoordinator {
|
|
|
1638
1778
|
this.finishing = true;
|
|
1639
1779
|
this.launchAbort?.abort(error);
|
|
1640
1780
|
this.dispatchAbort.abort(error);
|
|
1781
|
+
this.abortActiveDispatchOperations(error);
|
|
1641
1782
|
if (this.finishReject)
|
|
1642
1783
|
this.finishReject(error);
|
|
1643
1784
|
else
|
|
@@ -1648,6 +1789,7 @@ export class CrosscheckCoordinator {
|
|
|
1648
1789
|
if (!this.dispatchAbort.signal.aborted) {
|
|
1649
1790
|
this.dispatchAbort.abort(new Error("Crosscheck coordinator disposed"));
|
|
1650
1791
|
}
|
|
1792
|
+
this.abortActiveDispatchOperations(new Error("Crosscheck coordinator disposed"));
|
|
1651
1793
|
if (this.resultTimer)
|
|
1652
1794
|
clearInterval(this.resultTimer);
|
|
1653
1795
|
this.resultTimer = undefined;
|
|
@@ -1685,6 +1827,55 @@ export class CrosscheckCoordinator {
|
|
|
1685
1827
|
});
|
|
1686
1828
|
this.dispatchOperations.add(tracked);
|
|
1687
1829
|
}
|
|
1830
|
+
beginDispatchOperation() {
|
|
1831
|
+
const controller = new AbortController();
|
|
1832
|
+
const abortFromRoot = () => {
|
|
1833
|
+
controller.abort(this.dispatchAbort.signal.reason
|
|
1834
|
+
?? new Error("Crosscheck dispatch cancelled"));
|
|
1835
|
+
};
|
|
1836
|
+
if (this.dispatchAbort.signal.aborted) {
|
|
1837
|
+
abortFromRoot();
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
this.dispatchAbort.signal.addEventListener("abort", abortFromRoot, { once: true });
|
|
1841
|
+
}
|
|
1842
|
+
this.activeOperationAborts.add(controller);
|
|
1843
|
+
let released = false;
|
|
1844
|
+
return {
|
|
1845
|
+
fence: this.operationFence,
|
|
1846
|
+
signal: controller.signal,
|
|
1847
|
+
release: () => {
|
|
1848
|
+
if (released)
|
|
1849
|
+
return;
|
|
1850
|
+
released = true;
|
|
1851
|
+
this.dispatchAbort.signal.removeEventListener("abort", abortFromRoot);
|
|
1852
|
+
this.activeOperationAborts.delete(controller);
|
|
1853
|
+
},
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
currentDispatchOperationContext() {
|
|
1857
|
+
return {
|
|
1858
|
+
fence: this.operationFence,
|
|
1859
|
+
signal: this.dispatchAbort.signal,
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
isOperationCurrent(operation) {
|
|
1863
|
+
return operation.fence === this.operationFence
|
|
1864
|
+
&& !operation.signal.aborted
|
|
1865
|
+
&& !this.dispatchAbort.signal.aborted;
|
|
1866
|
+
}
|
|
1867
|
+
isCurrentGenerationOperation(generationId, operation, options = {}) {
|
|
1868
|
+
if (!this.isOperationCurrent(operation))
|
|
1869
|
+
return false;
|
|
1870
|
+
return this.store.isGenerationCurrent(generationId, options);
|
|
1871
|
+
}
|
|
1872
|
+
abortActiveDispatchOperations(reason) {
|
|
1873
|
+
this.operationFence++;
|
|
1874
|
+
for (const controller of this.activeOperationAborts) {
|
|
1875
|
+
controller.abort(reason);
|
|
1876
|
+
}
|
|
1877
|
+
this.activeOperationAborts.clear();
|
|
1878
|
+
}
|
|
1688
1879
|
startActivityHeartbeat() {
|
|
1689
1880
|
if (!this.options.touchActivity)
|
|
1690
1881
|
return;
|