@alexeiled/pi-fusion 0.7.0 → 0.8.0
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/README.md +4 -3
- package/docs/user-guide.md +15 -12
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +2 -1
- package/src/config.ts +26 -0
- package/src/fusion-args.ts +29 -2
- package/src/fusion-rpc.ts +29 -0
- package/src/index.ts +33 -5
- package/src/lifecycle-reconcile.ts +203 -6
- package/src/orchestrator.ts +487 -71
- package/src/panel-completion.ts +65 -17
- package/src/panel-quorum.ts +22 -0
- package/src/report.ts +85 -3
- package/src/result-extract.ts +66 -4
- package/src/run-builder.ts +84 -8
- package/src/run-store.ts +360 -11
- package/src/status.ts +1 -1
- package/src/types.ts +75 -0
package/src/orchestrator.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
} from "./config.js";
|
|
12
12
|
import { FusionArgsError } from "./errors.js";
|
|
13
13
|
import { parseFusionArgs } from "./fusion-args.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
decidePanelCompletion,
|
|
16
|
+
resolveMinimumSuccessfulPanelists,
|
|
17
|
+
} from "./panel-completion.js";
|
|
15
18
|
import {
|
|
16
19
|
renderCancelledReport,
|
|
17
20
|
renderFailureReport,
|
|
@@ -25,8 +28,16 @@ import {
|
|
|
25
28
|
reconcileIndexedLifecycleResult,
|
|
26
29
|
reconcilePanelResults,
|
|
27
30
|
} from "./lifecycle-reconcile.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
31
|
+
import {
|
|
32
|
+
appendThinkingSuffix,
|
|
33
|
+
buildPanelSpawnParams,
|
|
34
|
+
resolveEffectiveTimeouts,
|
|
35
|
+
} from "./run-builder.js";
|
|
36
|
+
import {
|
|
37
|
+
FusionRunStore,
|
|
38
|
+
FusionRunStoreError,
|
|
39
|
+
validateFusionRunPanelSlots,
|
|
40
|
+
} from "./run-store.js";
|
|
30
41
|
import {
|
|
31
42
|
clearFusionUi,
|
|
32
43
|
extractFusionProgressCounts,
|
|
@@ -45,6 +56,7 @@ import {
|
|
|
45
56
|
resolveSynthesisMode,
|
|
46
57
|
type FailedPanelSummary,
|
|
47
58
|
type FusionProfile,
|
|
59
|
+
type FusionProfileSnapshot,
|
|
48
60
|
type FusionRun,
|
|
49
61
|
type PanelOutput,
|
|
50
62
|
type ParsedFusionArgs,
|
|
@@ -113,6 +125,8 @@ export type FusionCommandResult =
|
|
|
113
125
|
interface RunLifecycleSnapshot {
|
|
114
126
|
statusPayload?: unknown;
|
|
115
127
|
resultPayload?: unknown;
|
|
128
|
+
/** Compact event results need stable slots; status/artifacts are ordered snapshots. */
|
|
129
|
+
resultSource?: "artifact" | "event" | "status";
|
|
116
130
|
resultIsTerminal: boolean;
|
|
117
131
|
resultArtifactPending?: boolean;
|
|
118
132
|
}
|
|
@@ -146,6 +160,11 @@ export class FusionOrchestrator {
|
|
|
146
160
|
this.context = ctx;
|
|
147
161
|
|
|
148
162
|
const args = typeof input === "string" ? parseFusionArgs(input) : input;
|
|
163
|
+
const inputError = validateStartArgs(args);
|
|
164
|
+
if (inputError) {
|
|
165
|
+
this.notify(ctx, inputError, "error");
|
|
166
|
+
return { status: "failed", error: inputError };
|
|
167
|
+
}
|
|
149
168
|
const existing = this.runStore.getActiveRun();
|
|
150
169
|
if (existing) {
|
|
151
170
|
const message = `Fusion run ${existing.id} is already active.`;
|
|
@@ -198,6 +217,7 @@ export class FusionOrchestrator {
|
|
|
198
217
|
|
|
199
218
|
const outputContract =
|
|
200
219
|
args.outputContract ?? detectCallerOutputContract(args.prompt);
|
|
220
|
+
const profileSnapshot = snapshotProfile(resolved.profile);
|
|
201
221
|
let run: FusionRun;
|
|
202
222
|
try {
|
|
203
223
|
run = this.runStore.startRun({
|
|
@@ -210,10 +230,24 @@ export class FusionOrchestrator {
|
|
|
210
230
|
? { operationId: args.operationId }
|
|
211
231
|
: {}),
|
|
212
232
|
...(outputContract ? { outputContract } : {}),
|
|
233
|
+
// profileSnapshot is the sole quorum record for new runs. The
|
|
234
|
+
// run-level field remains readable only for legacy snapshots.
|
|
235
|
+
profileSnapshot,
|
|
236
|
+
...(args.timeoutOverrides
|
|
237
|
+
? { timeoutOverrides: args.timeoutOverrides }
|
|
238
|
+
: {}),
|
|
239
|
+
effectiveTimeouts: resolveEffectiveTimeouts(
|
|
240
|
+
resolved.profile,
|
|
241
|
+
args.timeoutOverrides,
|
|
242
|
+
),
|
|
213
243
|
phase: "panel",
|
|
214
244
|
});
|
|
215
245
|
} catch (error: unknown) {
|
|
216
|
-
if (!(error instanceof FusionRunStoreError))
|
|
246
|
+
if (!(error instanceof FusionRunStoreError)) {
|
|
247
|
+
const message = errorMessage(error);
|
|
248
|
+
this.notify(ctx, message, "error");
|
|
249
|
+
return { status: "failed", error: message };
|
|
250
|
+
}
|
|
217
251
|
const active = this.runStore.getActiveRun();
|
|
218
252
|
if (active) {
|
|
219
253
|
this.notify(
|
|
@@ -225,12 +259,25 @@ export class FusionOrchestrator {
|
|
|
225
259
|
}
|
|
226
260
|
return { status: "failed", error: errorMessage(error) };
|
|
227
261
|
}
|
|
228
|
-
|
|
262
|
+
// Keep runtime behavior aligned with the exact durable profile that a
|
|
263
|
+
// restart will use, rather than retaining a mutable config object.
|
|
264
|
+
this.activeProfile = profileFromSnapshot(profileSnapshot);
|
|
229
265
|
publishFusionStatus(ctx, run);
|
|
230
266
|
|
|
231
267
|
try {
|
|
268
|
+
// Persist before the side effect. Public pi-subagents RPC has no
|
|
269
|
+
// correlation-key lookup, so restore treats this intent without its ID
|
|
270
|
+
// as unsafe to replay rather than creating an orphaned duplicate.
|
|
271
|
+
this.runStore.updateRun(run.id, {
|
|
272
|
+
spawnIntent: { stage: "panel", requestedAt: Date.now() },
|
|
273
|
+
});
|
|
232
274
|
const spawnResult = await this.rpc.spawn(
|
|
233
|
-
buildPanelSpawnParams(
|
|
275
|
+
buildPanelSpawnParams(
|
|
276
|
+
resolved.profile,
|
|
277
|
+
args.prompt,
|
|
278
|
+
outputContract,
|
|
279
|
+
args.timeoutOverrides,
|
|
280
|
+
),
|
|
234
281
|
);
|
|
235
282
|
const spawnError = extractSubagentFailure(spawnResult);
|
|
236
283
|
if (spawnError) throw new FusionArgsError(spawnError);
|
|
@@ -251,10 +298,19 @@ export class FusionOrchestrator {
|
|
|
251
298
|
? { status: "cancelled", run: cancelled, report: cancelled.report }
|
|
252
299
|
: { status: "ignored" };
|
|
253
300
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
let updated: FusionRun;
|
|
302
|
+
try {
|
|
303
|
+
updated = this.runStore.updateRun(run.id, {
|
|
304
|
+
spawnIntent: null,
|
|
305
|
+
panelRunId,
|
|
306
|
+
...(panelAsyncDir ? { panelAsyncDir } : {}),
|
|
307
|
+
});
|
|
308
|
+
} catch (persistenceError: unknown) {
|
|
309
|
+
// The remote run is now known but its ID is not durable. It cannot be
|
|
310
|
+
// safely recovered through the public RPC, so stop it before failing.
|
|
311
|
+
await this.stopOrphanedRun(panelRunId);
|
|
312
|
+
throw persistenceError;
|
|
313
|
+
}
|
|
258
314
|
publishFusionStatus(ctx, updated);
|
|
259
315
|
this.ensureReconcileLoop();
|
|
260
316
|
this.notify(
|
|
@@ -341,7 +397,7 @@ export class FusionOrchestrator {
|
|
|
341
397
|
this.context,
|
|
342
398
|
active,
|
|
343
399
|
progress,
|
|
344
|
-
deriveFusionStatusPhase(active, statusPayload),
|
|
400
|
+
deriveFusionStatusPhase(active, statusPayload, this.activeProfile),
|
|
345
401
|
);
|
|
346
402
|
}
|
|
347
403
|
return statusPayload;
|
|
@@ -419,34 +475,72 @@ export class FusionOrchestrator {
|
|
|
419
475
|
const summary = this.runStore.restoreFromSession(ctx);
|
|
420
476
|
this.clearActiveRuntime();
|
|
421
477
|
|
|
478
|
+
const restoreError = this.runStore.getRestoreError();
|
|
479
|
+
if (restoreError) {
|
|
480
|
+
this.configWarning = restoreError;
|
|
481
|
+
this.notify(ctx, restoreError, "warning");
|
|
482
|
+
clearFusionUi(ctx);
|
|
483
|
+
return summary;
|
|
484
|
+
}
|
|
485
|
+
|
|
422
486
|
const active = this.runStore.getActiveRun();
|
|
487
|
+
if (active && hasUnresolvedSpawnIntent(active)) {
|
|
488
|
+
const message = `Fusion recovery stopped: ${active.spawnIntent!.stage} spawn may have reached pi-subagents, but its run ID was not persisted. It will not be replayed because public RPC cannot safely adopt it.`;
|
|
489
|
+
this.failActiveRun(message);
|
|
490
|
+
this.notify(ctx, message, "warning");
|
|
491
|
+
return this.runStore.getLastRunSummary();
|
|
492
|
+
}
|
|
423
493
|
if (!active) {
|
|
424
494
|
this.stopReconcileLoop();
|
|
425
495
|
clearFusionUi(ctx);
|
|
426
496
|
return summary;
|
|
427
497
|
}
|
|
428
498
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
)
|
|
441
|
-
this.activeProfile = active.inlinePanel?.length
|
|
442
|
-
? buildInlinePanelProfile(base, active.inlinePanel)
|
|
443
|
-
: base;
|
|
499
|
+
const lifecycleError = validateRestoredRunLifecycle(active);
|
|
500
|
+
if (lifecycleError) {
|
|
501
|
+
this.failActiveRun(lifecycleError);
|
|
502
|
+
this.notify(ctx, lifecycleError, "warning");
|
|
503
|
+
return this.runStore.getLastRunSummary();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (active.profileSnapshot) {
|
|
507
|
+
// The snapshot was schema-validated while deserializing the run. It is
|
|
508
|
+
// the source of truth for labels, quorum, synthesis, and judge spawning;
|
|
509
|
+
// config edits made while a run is active must not rewrite that run.
|
|
510
|
+
this.activeProfile = profileFromSnapshot(active.profileSnapshot);
|
|
444
511
|
this.configWarning = undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
512
|
+
} else {
|
|
513
|
+
try {
|
|
514
|
+
const config = await this.loadConfig(ctx);
|
|
515
|
+
// Backward compatibility for sessions written before profile snapshots.
|
|
516
|
+
// An inline run's display name has no config entry, so rebuild it from
|
|
517
|
+
// its base profile plus persisted inline entries.
|
|
518
|
+
const base = this.resolveProfile(
|
|
519
|
+
config,
|
|
520
|
+
active.inlinePanel?.length
|
|
521
|
+
? active.baseProfileName
|
|
522
|
+
: active.profileName,
|
|
523
|
+
).profile;
|
|
524
|
+
this.activeProfile = active.inlinePanel?.length
|
|
525
|
+
? buildInlinePanelProfile(base, active.inlinePanel)
|
|
526
|
+
: base;
|
|
527
|
+
this.configWarning = undefined;
|
|
528
|
+
} catch (error: unknown) {
|
|
529
|
+
const message = `Could not restore legacy fusion profile "${active.profileName}": ${errorMessage(error)}`;
|
|
530
|
+
this.configWarning = message;
|
|
531
|
+
this.notify(ctx, message, "warning");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const panelSlotError = validateFusionRunPanelSlots(
|
|
535
|
+
active,
|
|
536
|
+
this.activeProfile?.panel.length ?? 0,
|
|
537
|
+
);
|
|
538
|
+
if (panelSlotError) {
|
|
539
|
+
this.failActiveRun(panelSlotError);
|
|
540
|
+
this.notify(ctx, panelSlotError, "warning");
|
|
541
|
+
return this.runStore.getLastRunSummary();
|
|
449
542
|
}
|
|
543
|
+
|
|
450
544
|
publishFusionStatus(ctx, active);
|
|
451
545
|
this.ensureReconcileLoop();
|
|
452
546
|
await this.reconcileActiveRun();
|
|
@@ -570,6 +664,7 @@ export class FusionOrchestrator {
|
|
|
570
664
|
eventPayload: payload,
|
|
571
665
|
});
|
|
572
666
|
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
667
|
+
this.persistVerifiedPanelResults(active, profile, snapshot.statusPayload);
|
|
573
668
|
const terminalPayload =
|
|
574
669
|
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
575
670
|
if (
|
|
@@ -589,6 +684,9 @@ export class FusionOrchestrator {
|
|
|
589
684
|
const extracted = extractPanelResults(lifecyclePayload, {
|
|
590
685
|
panel: profile.panel,
|
|
591
686
|
limit: profile.panel.length,
|
|
687
|
+
...(snapshot.resultSource === "event"
|
|
688
|
+
? { requireStableSlotIdentity: true }
|
|
689
|
+
: {}),
|
|
592
690
|
});
|
|
593
691
|
if (!extracted.ok) {
|
|
594
692
|
return this.failActiveRun(
|
|
@@ -601,7 +699,13 @@ export class FusionOrchestrator {
|
|
|
601
699
|
snapshot.statusPayload,
|
|
602
700
|
profile,
|
|
603
701
|
lifecyclePayload,
|
|
604
|
-
{
|
|
702
|
+
{
|
|
703
|
+
allowedTrailingResults: 1,
|
|
704
|
+
...(isWorkflowDeadline(lifecyclePayload) ||
|
|
705
|
+
isWorkflowDeadline(snapshot.statusPayload)
|
|
706
|
+
? { terminalizeRunning: true }
|
|
707
|
+
: {}),
|
|
708
|
+
},
|
|
605
709
|
);
|
|
606
710
|
if (!observedPanels.ok) {
|
|
607
711
|
return this.failActiveRun(
|
|
@@ -614,6 +718,26 @@ export class FusionOrchestrator {
|
|
|
614
718
|
observedPanels.failures,
|
|
615
719
|
);
|
|
616
720
|
|
|
721
|
+
// A legacy workflow may contain an embedded judge result even when the
|
|
722
|
+
// current persisted quorum policy would not permit synthesis. Apply the
|
|
723
|
+
// same completion decision as modern panel-only runs before accepting it.
|
|
724
|
+
const completion = decidePanelCompletion({
|
|
725
|
+
run: updated,
|
|
726
|
+
profile,
|
|
727
|
+
panelOutputs: observedPanels.outputs,
|
|
728
|
+
panelFailures: observedPanels.failures,
|
|
729
|
+
fallbackJudge: true,
|
|
730
|
+
});
|
|
731
|
+
if (completion.kind === "fail") {
|
|
732
|
+
return this.failActiveRun(completion.error, completion.report);
|
|
733
|
+
}
|
|
734
|
+
if (completion.kind === "complete") {
|
|
735
|
+
this.runStore.updateRun(updated.id, {
|
|
736
|
+
completionQuality: completionQuality(profile, observedPanels.outputs, observedPanels.failures),
|
|
737
|
+
});
|
|
738
|
+
return this.completeActiveRun(completion.report);
|
|
739
|
+
}
|
|
740
|
+
|
|
617
741
|
const judgePayload =
|
|
618
742
|
hasJudgeResult(snapshot.resultPayload, profile.panel.length)
|
|
619
743
|
? snapshot.resultPayload
|
|
@@ -641,6 +765,7 @@ export class FusionOrchestrator {
|
|
|
641
765
|
),
|
|
642
766
|
);
|
|
643
767
|
const observed = this.runStore.updateRun(updated.id, {
|
|
768
|
+
completionQuality: completionQuality(profile, observedPanels.outputs, observedPanels.failures),
|
|
644
769
|
judgeObservation,
|
|
645
770
|
});
|
|
646
771
|
const callerContract =
|
|
@@ -694,17 +819,23 @@ export class FusionOrchestrator {
|
|
|
694
819
|
});
|
|
695
820
|
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
696
821
|
|
|
822
|
+
const partial = this.persistVerifiedPanelResults(
|
|
823
|
+
active,
|
|
824
|
+
profile,
|
|
825
|
+
snapshot.statusPayload,
|
|
826
|
+
);
|
|
697
827
|
const panelIsTerminal =
|
|
698
828
|
snapshot.resultIsTerminal ||
|
|
699
829
|
isTerminalSubagentState(extractSubagentState(snapshot.statusPayload));
|
|
700
830
|
if (!active.panelStopReason && !panelIsTerminal) {
|
|
701
|
-
const partial = extractPanelResults(snapshot.statusPayload, {
|
|
702
|
-
panel: profile.panel,
|
|
703
|
-
completedOnly: true,
|
|
704
|
-
});
|
|
705
831
|
if (
|
|
706
|
-
partial
|
|
707
|
-
shouldStopWhenPanelAgrees(
|
|
832
|
+
partial &&
|
|
833
|
+
shouldStopWhenPanelAgrees(
|
|
834
|
+
profile,
|
|
835
|
+
partial.outputs,
|
|
836
|
+
partial.failures,
|
|
837
|
+
active.minimumSuccessfulPanelists,
|
|
838
|
+
)
|
|
708
839
|
) {
|
|
709
840
|
return this.stopPanelAfterAgreement(
|
|
710
841
|
active,
|
|
@@ -733,6 +864,11 @@ export class FusionOrchestrator {
|
|
|
733
864
|
const workflowStoppedIndices = extractWorkflowStoppedPanelIndices(
|
|
734
865
|
lifecyclePayload,
|
|
735
866
|
);
|
|
867
|
+
if (workflowStoppedIndices.some((index) => index >= profile.panel.length)) {
|
|
868
|
+
return this.failActiveRun(
|
|
869
|
+
"Terminal subagents data referenced a panel slot outside the configured panel.",
|
|
870
|
+
);
|
|
871
|
+
}
|
|
736
872
|
const stoppedPanelIndices =
|
|
737
873
|
active.panelStoppedIndices ??
|
|
738
874
|
(workflowStoppedIndices.length > 0 ? workflowStoppedIndices : undefined);
|
|
@@ -740,6 +876,9 @@ export class FusionOrchestrator {
|
|
|
740
876
|
panel: profile.panel,
|
|
741
877
|
limit: profile.panel.length,
|
|
742
878
|
...(stoppedPanelIndices ? { stoppedPanelIndices } : {}),
|
|
879
|
+
...(snapshot.resultSource === "event"
|
|
880
|
+
? { requireStableSlotIdentity: true }
|
|
881
|
+
: {}),
|
|
743
882
|
});
|
|
744
883
|
if (!extracted.ok) {
|
|
745
884
|
return this.failActiveRun(
|
|
@@ -752,17 +891,31 @@ export class FusionOrchestrator {
|
|
|
752
891
|
snapshot.statusPayload,
|
|
753
892
|
profile,
|
|
754
893
|
lifecyclePayload,
|
|
755
|
-
{
|
|
894
|
+
{
|
|
895
|
+
...(stoppedPanelIndices ? { stoppedPanelIndices } : {}),
|
|
896
|
+
...(isWorkflowDeadline(lifecyclePayload) ||
|
|
897
|
+
isWorkflowDeadline(snapshot.statusPayload)
|
|
898
|
+
? { terminalizeRunning: true }
|
|
899
|
+
: {}),
|
|
900
|
+
},
|
|
756
901
|
);
|
|
757
902
|
if (!observedPanels.ok) {
|
|
758
903
|
return this.failActiveRun(
|
|
759
904
|
`${observedPanels.error.message} (${observedPanels.error.path})`,
|
|
760
905
|
);
|
|
761
906
|
}
|
|
907
|
+
const reconciledFailures = withWorkflowDeadlineFailures(
|
|
908
|
+
observedPanels.failures,
|
|
909
|
+
isWorkflowDeadline(lifecyclePayload) ||
|
|
910
|
+
isWorkflowDeadline(snapshot.statusPayload)
|
|
911
|
+
? extractSubagentFailure(snapshot.statusPayload) ??
|
|
912
|
+
extractSubagentFailure(lifecyclePayload)
|
|
913
|
+
: undefined,
|
|
914
|
+
);
|
|
762
915
|
const stored = this.storePanelResults(
|
|
763
916
|
active.id,
|
|
764
917
|
observedPanels.outputs,
|
|
765
|
-
|
|
918
|
+
reconciledFailures,
|
|
766
919
|
);
|
|
767
920
|
const updated =
|
|
768
921
|
workflowStoppedIndices.length > 0 && !active.panelStopReason
|
|
@@ -776,7 +929,7 @@ export class FusionOrchestrator {
|
|
|
776
929
|
updated,
|
|
777
930
|
profile,
|
|
778
931
|
observedPanels.outputs,
|
|
779
|
-
|
|
932
|
+
reconciledFailures,
|
|
780
933
|
{ fallbackJudge: false },
|
|
781
934
|
);
|
|
782
935
|
}
|
|
@@ -854,10 +1007,16 @@ export class FusionOrchestrator {
|
|
|
854
1007
|
return this.failActiveRun(decision.error, decision.report);
|
|
855
1008
|
}
|
|
856
1009
|
if (decision.kind === "complete") {
|
|
1010
|
+
this.runStore.updateRun(run.id, {
|
|
1011
|
+
completionQuality: completionQuality(profile, panelOutputs, panelFailures),
|
|
1012
|
+
});
|
|
857
1013
|
return this.completeActiveRun(decision.report);
|
|
858
1014
|
}
|
|
859
1015
|
|
|
860
1016
|
try {
|
|
1017
|
+
this.runStore.updateRun(run.id, {
|
|
1018
|
+
spawnIntent: { stage: "judge", requestedAt: Date.now() },
|
|
1019
|
+
});
|
|
861
1020
|
const spawnResult = await this.rpc.spawn(decision.params);
|
|
862
1021
|
const spawnError = extractSubagentFailure(spawnResult);
|
|
863
1022
|
if (spawnError) throw new FusionArgsError(spawnError);
|
|
@@ -870,13 +1029,23 @@ export class FusionOrchestrator {
|
|
|
870
1029
|
await this.stopOrphanedRun(judgeRunId, "judge");
|
|
871
1030
|
return { status: "ignored" };
|
|
872
1031
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1032
|
+
let nextRun: FusionRun;
|
|
1033
|
+
try {
|
|
1034
|
+
nextRun = this.runStore.updateRun(run.id, {
|
|
1035
|
+
spawnIntent: null,
|
|
1036
|
+
phase: "judge",
|
|
1037
|
+
completionQuality: completionQuality(profile, panelOutputs, panelFailures),
|
|
1038
|
+
judgeRunId,
|
|
1039
|
+
...(judgeAsyncDir ? { judgeAsyncDir } : {}),
|
|
1040
|
+
panelOutputs: [...panelOutputs],
|
|
1041
|
+
panelFailures: [...panelFailures],
|
|
1042
|
+
});
|
|
1043
|
+
} catch (persistenceError: unknown) {
|
|
1044
|
+
// As for the panel, never leave a remotely started synthesis run
|
|
1045
|
+
// alive when recording its public ID failed.
|
|
1046
|
+
await this.stopOrphanedRun(judgeRunId, "judge");
|
|
1047
|
+
throw persistenceError;
|
|
1048
|
+
}
|
|
880
1049
|
publishFusionStatus(this.context, nextRun);
|
|
881
1050
|
this.notify(
|
|
882
1051
|
this.context,
|
|
@@ -1005,7 +1174,7 @@ export class FusionOrchestrator {
|
|
|
1005
1174
|
this.context,
|
|
1006
1175
|
input.run,
|
|
1007
1176
|
progress,
|
|
1008
|
-
deriveFusionStatusPhase(input.run, statusPayload),
|
|
1177
|
+
deriveFusionStatusPhase(input.run, statusPayload, this.activeProfile),
|
|
1009
1178
|
);
|
|
1010
1179
|
}
|
|
1011
1180
|
|
|
@@ -1015,23 +1184,20 @@ export class FusionOrchestrator {
|
|
|
1015
1184
|
const eventIsTerminal =
|
|
1016
1185
|
isTerminalSubagentState(extractSubagentState(input.eventPayload)) ||
|
|
1017
1186
|
isTerminalFusionProgress(input.eventPayload);
|
|
1018
|
-
const eventHasResults =
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
resultIsTerminal: eventIsTerminal || statusIsTerminal,
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1187
|
+
const eventHasResults = hasExplicitResultsArray(input.eventPayload);
|
|
1188
|
+
// Completion events are compact and can truncate long inline outputs. A
|
|
1189
|
+
// result artifact is the complete terminal record, so select it before an
|
|
1190
|
+
// event and use it as the reconciliation snapshot as well. Otherwise a
|
|
1191
|
+
// partial status/event could discard verified slots or the judge end tag.
|
|
1027
1192
|
const artifactResult = readSubagentResultArtifact({
|
|
1028
1193
|
...(input.runId ? { runId: input.runId } : {}),
|
|
1029
1194
|
...(input.asyncDir ? { asyncDir: input.asyncDir } : {}),
|
|
1030
1195
|
});
|
|
1031
|
-
if (
|
|
1196
|
+
if (hasExplicitResultsArray(artifactResult)) {
|
|
1032
1197
|
return {
|
|
1033
|
-
statusPayload,
|
|
1198
|
+
statusPayload: artifactResult,
|
|
1034
1199
|
resultPayload: artifactResult,
|
|
1200
|
+
resultSource: "artifact",
|
|
1035
1201
|
resultIsTerminal: true,
|
|
1036
1202
|
};
|
|
1037
1203
|
}
|
|
@@ -1047,10 +1213,20 @@ export class FusionOrchestrator {
|
|
|
1047
1213
|
};
|
|
1048
1214
|
}
|
|
1049
1215
|
|
|
1050
|
-
if (
|
|
1216
|
+
if (eventPayloadMatches && eventHasResults) {
|
|
1217
|
+
return {
|
|
1218
|
+
statusPayload,
|
|
1219
|
+
resultPayload: input.eventPayload,
|
|
1220
|
+
resultSource: "event",
|
|
1221
|
+
resultIsTerminal: eventIsTerminal || statusIsTerminal,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (hasExplicitResultsArray(statusPayload)) {
|
|
1051
1226
|
return {
|
|
1052
1227
|
statusPayload,
|
|
1053
1228
|
resultPayload: statusPayload,
|
|
1229
|
+
resultSource: "status",
|
|
1054
1230
|
resultIsTerminal: statusIsTerminal,
|
|
1055
1231
|
};
|
|
1056
1232
|
}
|
|
@@ -1074,14 +1250,64 @@ export class FusionOrchestrator {
|
|
|
1074
1250
|
return { statusPayload, resultIsTerminal: false };
|
|
1075
1251
|
}
|
|
1076
1252
|
|
|
1253
|
+
/** Persists every terminal child slot observed while the workflow continues. */
|
|
1254
|
+
private persistVerifiedPanelResults(
|
|
1255
|
+
run: FusionRun,
|
|
1256
|
+
profile: FusionProfile,
|
|
1257
|
+
statusPayload: unknown,
|
|
1258
|
+
): ExtractPanelResultsSuccess | undefined {
|
|
1259
|
+
if (statusPayload === undefined) return undefined;
|
|
1260
|
+
const partial = extractPanelResults(statusPayload, {
|
|
1261
|
+
panel: profile.panel,
|
|
1262
|
+
completedOnly: true,
|
|
1263
|
+
limit: profile.panel.length,
|
|
1264
|
+
});
|
|
1265
|
+
if (!partial.ok) return undefined;
|
|
1266
|
+
if (partial.outputs.length > 0 || partial.failures.length > 0) {
|
|
1267
|
+
this.storePanelResults(run.id, partial.outputs, partial.failures);
|
|
1268
|
+
}
|
|
1269
|
+
return partial;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1077
1272
|
private storePanelResults(
|
|
1078
1273
|
runId: string,
|
|
1079
1274
|
panelOutputs: readonly PanelOutput[],
|
|
1080
1275
|
panelFailures: readonly FailedPanelSummary[],
|
|
1081
1276
|
): FusionRun {
|
|
1277
|
+
// Lifecycle status is append-only in intent but not guaranteed to repeat
|
|
1278
|
+
// older slots on every poll. Keep persisted verified slots until a newer
|
|
1279
|
+
// observation for that exact stable index supersedes them.
|
|
1280
|
+
const existing = this.runStore.getActiveRun();
|
|
1281
|
+
const slots = new Map<
|
|
1282
|
+
number,
|
|
1283
|
+
{ output?: PanelOutput; failure?: FailedPanelSummary }
|
|
1284
|
+
>();
|
|
1285
|
+
for (const output of existing?.panelOutputs ?? []) {
|
|
1286
|
+
slots.set(output.index, { output });
|
|
1287
|
+
}
|
|
1288
|
+
for (const failure of existing?.panelFailures ?? []) {
|
|
1289
|
+
slots.set(failure.index, { failure });
|
|
1290
|
+
}
|
|
1291
|
+
for (const output of panelOutputs) slots.set(output.index, { output });
|
|
1292
|
+
for (const failure of panelFailures) slots.set(failure.index, { failure });
|
|
1293
|
+
const mergedOutputs = Array.from(slots.values())
|
|
1294
|
+
.flatMap((slot) => (slot.output ? [slot.output] : []))
|
|
1295
|
+
.sort((left, right) => left.index - right.index);
|
|
1296
|
+
const mergedFailures = Array.from(slots.values())
|
|
1297
|
+
.flatMap((slot) => (slot.failure ? [slot.failure] : []))
|
|
1298
|
+
.sort((left, right) => left.index - right.index);
|
|
1299
|
+
// Status polling repeats complete snapshots. Persist only a material slot
|
|
1300
|
+
// change so a long-running restore does not append identical session data.
|
|
1301
|
+
if (
|
|
1302
|
+
existing?.id === runId &&
|
|
1303
|
+
sameSnapshot(existing.panelOutputs, mergedOutputs) &&
|
|
1304
|
+
sameSnapshot(existing.panelFailures, mergedFailures)
|
|
1305
|
+
) {
|
|
1306
|
+
return existing;
|
|
1307
|
+
}
|
|
1082
1308
|
return this.runStore.updateRun(runId, {
|
|
1083
|
-
panelOutputs:
|
|
1084
|
-
panelFailures:
|
|
1309
|
+
panelOutputs: mergedOutputs,
|
|
1310
|
+
panelFailures: mergedFailures,
|
|
1085
1311
|
});
|
|
1086
1312
|
}
|
|
1087
1313
|
|
|
@@ -1112,6 +1338,12 @@ export class FusionOrchestrator {
|
|
|
1112
1338
|
...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
|
|
1113
1339
|
...(active.panelRunId ? { panelRunId: active.panelRunId } : {}),
|
|
1114
1340
|
...(active.judgeRunId ? { judgeRunId: active.judgeRunId } : {}),
|
|
1341
|
+
recovery: {
|
|
1342
|
+
retryDeferred: true,
|
|
1343
|
+
failedPanelIndices: storedPanelFailures(active)
|
|
1344
|
+
.map((failure) => failure.index)
|
|
1345
|
+
.sort((left, right) => left - right),
|
|
1346
|
+
},
|
|
1115
1347
|
report,
|
|
1116
1348
|
error,
|
|
1117
1349
|
});
|
|
@@ -1199,6 +1431,124 @@ export class FusionOrchestrator {
|
|
|
1199
1431
|
}
|
|
1200
1432
|
}
|
|
1201
1433
|
|
|
1434
|
+
function sameSnapshot<T>(
|
|
1435
|
+
left: readonly T[] | undefined,
|
|
1436
|
+
right: readonly T[],
|
|
1437
|
+
): boolean {
|
|
1438
|
+
return JSON.stringify(left ?? []) === JSON.stringify(right);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/** Captures only settings that can affect post-restart reconciliation/reporting. */
|
|
1442
|
+
function snapshotProfile(profile: FusionProfile): FusionProfileSnapshot {
|
|
1443
|
+
return {
|
|
1444
|
+
panel: profile.panel.map((member) => ({ ...member })),
|
|
1445
|
+
judge: { ...profile.judge },
|
|
1446
|
+
minimumSuccessfulPanelists: resolveMinimumSuccessfulPanelists(
|
|
1447
|
+
profile.minimumSuccessfulPanelists,
|
|
1448
|
+
profile.panel.length,
|
|
1449
|
+
),
|
|
1450
|
+
...(profile.context !== undefined ? { context: profile.context } : {}),
|
|
1451
|
+
...(profile.stopWhenPanelAgrees !== undefined
|
|
1452
|
+
? { stopWhenPanelAgrees: profile.stopWhenPanelAgrees }
|
|
1453
|
+
: {}),
|
|
1454
|
+
...(profile.blindPanelLabels !== undefined
|
|
1455
|
+
? { blindPanelLabels: profile.blindPanelLabels }
|
|
1456
|
+
: {}),
|
|
1457
|
+
...(profile.judgeToolBudget !== undefined
|
|
1458
|
+
? {
|
|
1459
|
+
judgeToolBudget: {
|
|
1460
|
+
...profile.judgeToolBudget,
|
|
1461
|
+
...(Array.isArray(profile.judgeToolBudget.block)
|
|
1462
|
+
? { block: [...profile.judgeToolBudget.block] }
|
|
1463
|
+
: {}),
|
|
1464
|
+
},
|
|
1465
|
+
}
|
|
1466
|
+
: {}),
|
|
1467
|
+
...(profile.synthesis !== undefined ? { synthesis: profile.synthesis } : {}),
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function profileFromSnapshot(snapshot: FusionProfileSnapshot): FusionProfile {
|
|
1472
|
+
return {
|
|
1473
|
+
panel: snapshot.panel.map((member) => ({ ...member })),
|
|
1474
|
+
judge: { ...snapshot.judge },
|
|
1475
|
+
minimumSuccessfulPanelists: snapshot.minimumSuccessfulPanelists,
|
|
1476
|
+
...(snapshot.context !== undefined ? { context: snapshot.context } : {}),
|
|
1477
|
+
...(snapshot.stopWhenPanelAgrees !== undefined
|
|
1478
|
+
? { stopWhenPanelAgrees: snapshot.stopWhenPanelAgrees }
|
|
1479
|
+
: {}),
|
|
1480
|
+
...(snapshot.blindPanelLabels !== undefined
|
|
1481
|
+
? { blindPanelLabels: snapshot.blindPanelLabels }
|
|
1482
|
+
: {}),
|
|
1483
|
+
...(snapshot.judgeToolBudget !== undefined
|
|
1484
|
+
? {
|
|
1485
|
+
judgeToolBudget: {
|
|
1486
|
+
...snapshot.judgeToolBudget,
|
|
1487
|
+
...(Array.isArray(snapshot.judgeToolBudget.block)
|
|
1488
|
+
? { block: [...snapshot.judgeToolBudget.block] }
|
|
1489
|
+
: {}),
|
|
1490
|
+
},
|
|
1491
|
+
}
|
|
1492
|
+
: {}),
|
|
1493
|
+
...(snapshot.synthesis !== undefined ? { synthesis: snapshot.synthesis } : {}),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function completionQuality(
|
|
1498
|
+
profile: FusionProfile,
|
|
1499
|
+
outputs: readonly PanelOutput[],
|
|
1500
|
+
failures: readonly FailedPanelSummary[],
|
|
1501
|
+
): "complete" | "partial" {
|
|
1502
|
+
return outputs.length === profile.panel.length ||
|
|
1503
|
+
(profile.stopWhenPanelAgrees === true &&
|
|
1504
|
+
failures.length > 0 &&
|
|
1505
|
+
failures.every((failure) => failure.reason === "stopped-after-agreement"))
|
|
1506
|
+
? "complete"
|
|
1507
|
+
: "partial";
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function validateStartArgs(args: ParsedFusionArgs): string | undefined {
|
|
1511
|
+
if (typeof args.prompt !== "string" || !args.prompt.trim()) {
|
|
1512
|
+
return "Fusion prompt must not be blank.";
|
|
1513
|
+
}
|
|
1514
|
+
if (args.profile !== undefined && !args.profile.trim()) {
|
|
1515
|
+
return "Fusion profile must not be blank.";
|
|
1516
|
+
}
|
|
1517
|
+
if (
|
|
1518
|
+
args.panel !== undefined &&
|
|
1519
|
+
(args.panel.length === 0 || args.panel.some((entry) => !entry.trim()))
|
|
1520
|
+
) {
|
|
1521
|
+
return "Fusion panel must contain at least one non-blank entry.";
|
|
1522
|
+
}
|
|
1523
|
+
return undefined;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function hasUnresolvedSpawnIntent(run: FusionRun): boolean {
|
|
1527
|
+
if (!run.spawnIntent) return false;
|
|
1528
|
+
return run.spawnIntent.stage === "panel"
|
|
1529
|
+
? !run.panelRunId && !run.chainRunId
|
|
1530
|
+
: !run.judgeRunId;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* A restored nonterminal phase without the remote ID cannot be reconciled.
|
|
1535
|
+
* Spawn intents are handled first so their more specific no-replay failure is
|
|
1536
|
+
* preserved; all other incomplete records are terminalized rather than left
|
|
1537
|
+
* active forever.
|
|
1538
|
+
*/
|
|
1539
|
+
function validateRestoredRunLifecycle(run: FusionRun): string | undefined {
|
|
1540
|
+
if (run.phase === "judge" && !run.judgeRunId) {
|
|
1541
|
+
return "Fusion recovery stopped: judge phase has no persisted judge run ID.";
|
|
1542
|
+
}
|
|
1543
|
+
if (run.phase === "chain" && !run.chainRunId) {
|
|
1544
|
+
return "Fusion recovery stopped: chain phase has no persisted chain run ID.";
|
|
1545
|
+
}
|
|
1546
|
+
if (run.phase === "panel" && !run.panelRunId && !run.chainRunId) {
|
|
1547
|
+
return "Fusion recovery stopped: panel phase has no persisted panel run ID.";
|
|
1548
|
+
}
|
|
1549
|
+
return undefined;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1202
1552
|
export function extractSubagentRunId(payload: unknown): string | undefined {
|
|
1203
1553
|
if (!isRecord(payload)) return undefined;
|
|
1204
1554
|
const direct = firstNonBlankString(
|
|
@@ -1303,14 +1653,17 @@ function formatFusionStatusReport(input: {
|
|
|
1303
1653
|
);
|
|
1304
1654
|
lines.push(`Profile: ${input.active.profileName}`);
|
|
1305
1655
|
lines.push(`Phase: ${input.details?.phaseLabel ?? input.active.phase}`);
|
|
1656
|
+
appendEffectiveTimeouts(lines, input.active);
|
|
1306
1657
|
if (input.active.chainRunId)
|
|
1307
1658
|
lines.push(`Chain run: ${input.active.chainRunId}`);
|
|
1308
1659
|
else if (input.active.panelRunId)
|
|
1309
1660
|
lines.push(`Panel run: ${input.active.panelRunId}`);
|
|
1310
1661
|
if (input.active.judgeRunId) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1662
|
+
const synthesisLabel =
|
|
1663
|
+
input.details?.fallbackJudge?.label ??
|
|
1664
|
+
input.details?.judge?.label ??
|
|
1665
|
+
(input.active.chainRunId ? "Fallback judge" : "Judge");
|
|
1666
|
+
lines.push(`${synthesisLabel} run: ${input.active.judgeRunId}`);
|
|
1314
1667
|
}
|
|
1315
1668
|
lines.push(
|
|
1316
1669
|
`Progress: ${input.progress ? formatProgressCounts(input.progress) : "unknown"}`,
|
|
@@ -1322,6 +1675,7 @@ function formatFusionStatusReport(input: {
|
|
|
1322
1675
|
lines.push(`Prompt: ${firstLine(input.last.prompt)}`);
|
|
1323
1676
|
lines.push(`Profile: ${input.last.profileName}`);
|
|
1324
1677
|
lines.push(`Phase: ${input.last.phase}`);
|
|
1678
|
+
appendEffectiveTimeouts(lines, input.last);
|
|
1325
1679
|
if (input.last.chainRunId)
|
|
1326
1680
|
lines.push(`Chain run: ${input.last.chainRunId}`);
|
|
1327
1681
|
else if (input.last.panelRunId)
|
|
@@ -1343,6 +1697,20 @@ function formatFusionStatusReport(input: {
|
|
|
1343
1697
|
return lines.join("\n");
|
|
1344
1698
|
}
|
|
1345
1699
|
|
|
1700
|
+
function appendEffectiveTimeouts(
|
|
1701
|
+
lines: string[],
|
|
1702
|
+
run: Pick<FusionRun, "effectiveTimeouts">,
|
|
1703
|
+
): void {
|
|
1704
|
+
const timeouts = run.effectiveTimeouts;
|
|
1705
|
+
if (!timeouts) return;
|
|
1706
|
+
lines.push(
|
|
1707
|
+
`Deadlines: panelist ${timeouts.panelistTimeoutMs}ms, panel ${timeouts.panelTimeoutMs}ms (+${timeouts.panelGraceMs}ms grace), judge ${timeouts.judgeTimeoutMs}ms`,
|
|
1708
|
+
);
|
|
1709
|
+
if (timeouts.usesLegacyTimeout) {
|
|
1710
|
+
lines.push("Warning: legacy timeoutMs supplied one or more effective deadlines.");
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1346
1714
|
function appendStatusDetails(
|
|
1347
1715
|
lines: string[],
|
|
1348
1716
|
details: FusionStatusDetails | undefined,
|
|
@@ -1397,7 +1765,7 @@ function buildFusionStatusDetails(
|
|
|
1397
1765
|
): FusionStatusDetails {
|
|
1398
1766
|
const details: FusionStatusDetails = {
|
|
1399
1767
|
prompt: run.prompt,
|
|
1400
|
-
phaseLabel: deriveFusionStatusPhase(run, payload),
|
|
1768
|
+
phaseLabel: deriveFusionStatusPhase(run, payload, profile),
|
|
1401
1769
|
};
|
|
1402
1770
|
if (!profile) return details;
|
|
1403
1771
|
|
|
@@ -1410,7 +1778,7 @@ function buildFusionStatusDetails(
|
|
|
1410
1778
|
storedPanelFailures(run),
|
|
1411
1779
|
);
|
|
1412
1780
|
details.fallbackJudge = {
|
|
1413
|
-
label: run.chainRunId
|
|
1781
|
+
label: synthesisStatusLabel(profile, Boolean(run.chainRunId)),
|
|
1414
1782
|
...(judgeModel ? { model: judgeModel } : {}),
|
|
1415
1783
|
status: describeStandaloneRunStatus(payload),
|
|
1416
1784
|
};
|
|
@@ -1439,7 +1807,7 @@ function buildFusionStatusDetails(
|
|
|
1439
1807
|
const judgeActivity = describeStepActivity(steps[profile.panel.length]);
|
|
1440
1808
|
const judgeMetrics = describeStepMetrics(steps[profile.panel.length]);
|
|
1441
1809
|
details.judge = {
|
|
1442
|
-
label:
|
|
1810
|
+
label: synthesisStatusLabel(profile),
|
|
1443
1811
|
...(judgeModel ? { model: judgeModel } : {}),
|
|
1444
1812
|
status: describeChainJudgeStatus(steps[profile.panel.length], panelists),
|
|
1445
1813
|
...([judgeActivity, judgeMetrics].filter(Boolean).length > 0
|
|
@@ -1452,9 +1820,10 @@ function buildFusionStatusDetails(
|
|
|
1452
1820
|
function deriveFusionStatusPhase(
|
|
1453
1821
|
run: Pick<FusionRun, "phase" | "chainRunId">,
|
|
1454
1822
|
payload: unknown,
|
|
1823
|
+
profile?: Pick<FusionProfile, "panel" | "synthesis">,
|
|
1455
1824
|
): string {
|
|
1456
1825
|
if (run.phase === "judge") {
|
|
1457
|
-
return run.chainRunId
|
|
1826
|
+
return synthesisStatusLabel(profile, Boolean(run.chainRunId)).toLowerCase();
|
|
1458
1827
|
}
|
|
1459
1828
|
if (run.phase !== "chain") return run.phase;
|
|
1460
1829
|
|
|
@@ -1462,7 +1831,7 @@ function deriveFusionStatusPhase(
|
|
|
1462
1831
|
if (steps.length === 0) return "chain";
|
|
1463
1832
|
const judgeStatus = normalizeStatusLabel(steps.at(-1));
|
|
1464
1833
|
if (judgeStatus === "running" || judgeStatus === "completed") {
|
|
1465
|
-
return
|
|
1834
|
+
return synthesisStatusLabel(profile).toLowerCase();
|
|
1466
1835
|
}
|
|
1467
1836
|
const panelSteps = steps.slice(0, -1);
|
|
1468
1837
|
if (panelSteps.some((step) => normalizeStatusLabel(step) === "running")) {
|
|
@@ -1473,11 +1842,23 @@ function deriveFusionStatusPhase(
|
|
|
1473
1842
|
const status = normalizeStatusLabel(step);
|
|
1474
1843
|
return status === "completed" || status === "failed";
|
|
1475
1844
|
});
|
|
1476
|
-
return allPanelsFinished
|
|
1845
|
+
return allPanelsFinished
|
|
1846
|
+
? synthesisStatusLabel(profile).toLowerCase()
|
|
1847
|
+
: "panel";
|
|
1477
1848
|
}
|
|
1478
1849
|
return "panel";
|
|
1479
1850
|
}
|
|
1480
1851
|
|
|
1852
|
+
function synthesisStatusLabel(
|
|
1853
|
+
profile: Pick<FusionProfile, "panel" | "synthesis"> | undefined,
|
|
1854
|
+
fallback = false,
|
|
1855
|
+
): "Judge" | "Fallback judge" | "Composer" | "Fallback composer" {
|
|
1856
|
+
const base = profile && resolveSynthesisMode(profile) === "merge"
|
|
1857
|
+
? "Composer"
|
|
1858
|
+
: "Judge";
|
|
1859
|
+
return fallback ? `Fallback ${base.toLowerCase()}` as "Fallback judge" | "Fallback composer" : base;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1481
1862
|
function buildCompletedPanelStatusLines(
|
|
1482
1863
|
panel: FusionProfile["panel"],
|
|
1483
1864
|
outputs: readonly PanelOutput[],
|
|
@@ -1635,6 +2016,10 @@ function findResultsArray(payload: unknown): readonly unknown[] | undefined {
|
|
|
1635
2016
|
return undefined;
|
|
1636
2017
|
}
|
|
1637
2018
|
|
|
2019
|
+
function hasExplicitResultsArray(payload: unknown): boolean {
|
|
2020
|
+
return findResultsArray(payload) !== undefined;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1638
2023
|
function hasResultsArray(payload: unknown): boolean {
|
|
1639
2024
|
return (findResultsArray(payload)?.length ?? 0) > 0;
|
|
1640
2025
|
}
|
|
@@ -1745,6 +2130,31 @@ function extractWorkflowStoppedPanelIndices(payload: unknown): number[] {
|
|
|
1745
2130
|
return [];
|
|
1746
2131
|
}
|
|
1747
2132
|
|
|
2133
|
+
function withWorkflowDeadlineFailures(
|
|
2134
|
+
failures: readonly FailedPanelSummary[],
|
|
2135
|
+
deadlineError: string | undefined,
|
|
2136
|
+
): FailedPanelSummary[] {
|
|
2137
|
+
if (!deadlineError) return [...failures];
|
|
2138
|
+
return failures.map((failure) => ({
|
|
2139
|
+
...failure,
|
|
2140
|
+
summary: failure.summary.includes(deadlineError)
|
|
2141
|
+
? failure.summary
|
|
2142
|
+
: `${deadlineError}\n${failure.summary}`,
|
|
2143
|
+
reason: failure.reason ?? "timeout",
|
|
2144
|
+
}));
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function isWorkflowDeadline(payload: unknown): boolean {
|
|
2148
|
+
if (!isRecord(payload)) return false;
|
|
2149
|
+
if (payload.timedOut === true) return true;
|
|
2150
|
+
const error = firstNonBlankString(payload.error, payload.errorMessage);
|
|
2151
|
+
if (error && /(?:workflow.*(?:timed out|timeout)|(?:timed out|timeout).*workflow)/i.test(error)) {
|
|
2152
|
+
return true;
|
|
2153
|
+
}
|
|
2154
|
+
if (isRecord(payload.details) && isWorkflowDeadline(payload.details)) return true;
|
|
2155
|
+
return isRecord(payload.data) ? isWorkflowDeadline(payload.data) : false;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1748
2158
|
function extractSubagentFailure(payload: unknown): string | undefined {
|
|
1749
2159
|
if (!isRecord(payload)) return undefined;
|
|
1750
2160
|
const direct = firstNonBlankString(payload.error, payload.errorMessage);
|
|
@@ -1823,9 +2233,15 @@ function shouldStopWhenPanelAgrees(
|
|
|
1823
2233
|
profile: FusionProfile,
|
|
1824
2234
|
outputs: readonly PanelOutput[],
|
|
1825
2235
|
failures: readonly FailedPanelSummary[],
|
|
2236
|
+
persistedPolicy?: FusionRun["minimumSuccessfulPanelists"],
|
|
1826
2237
|
): boolean {
|
|
2238
|
+
const required = resolveMinimumSuccessfulPanelists(
|
|
2239
|
+
persistedPolicy ?? profile.minimumSuccessfulPanelists,
|
|
2240
|
+
profile.panel.length,
|
|
2241
|
+
);
|
|
1827
2242
|
return (
|
|
1828
2243
|
profile.stopWhenPanelAgrees === true &&
|
|
2244
|
+
outputs.length >= required &&
|
|
1829
2245
|
hasStrongPanelAgreement(
|
|
1830
2246
|
outputs,
|
|
1831
2247
|
outputs.length + failures.length,
|