@alexeiled/pi-fusion 0.6.2 → 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/src/run-store.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type {
3
3
  FusionPhase,
4
+ FusionProfileSnapshot,
5
+ FusionRecoveryState,
4
6
  FusionRun,
5
7
  ModelAttempt,
6
8
  PanelDecision,
@@ -24,6 +26,10 @@ export type FusionRunSummary = Omit<
24
26
  | "prompt"
25
27
  | "profileName"
26
28
  | "operationId"
29
+ | "outputContract"
30
+ | "completionQuality"
31
+ | "effectiveTimeouts"
32
+ | "recovery"
27
33
  | "phase"
28
34
  | "createdAt"
29
35
  | "updatedAt"
@@ -43,6 +49,11 @@ export interface FusionRunStartInput {
43
49
  inlinePanel?: string[];
44
50
  baseProfileName?: string;
45
51
  operationId?: string;
52
+ outputContract?: FusionRun["outputContract"];
53
+ minimumSuccessfulPanelists?: FusionRun["minimumSuccessfulPanelists"];
54
+ profileSnapshot?: FusionRun["profileSnapshot"];
55
+ timeoutOverrides?: FusionRun["timeoutOverrides"];
56
+ effectiveTimeouts?: FusionRun["effectiveTimeouts"];
46
57
  phase?: Exclude<FusionPhase, FusionTerminalPhase>;
47
58
  createdAt?: number;
48
59
  }
@@ -58,8 +69,12 @@ export interface FusionRunPatch {
58
69
  judgeRunId?: string;
59
70
  judgeAsyncDir?: string;
60
71
  judgeObservation?: FusionRun["judgeObservation"];
72
+ completionQuality?: FusionRun["completionQuality"];
61
73
  panelOutputs?: FusionRun["panelOutputs"];
62
74
  panelFailures?: FusionRun["panelFailures"];
75
+ recovery?: FusionRun["recovery"];
76
+ /** `null` clears an intent once the RPC returned its durable remote ID. */
77
+ spawnIntent?: FusionRun["spawnIntent"] | null;
63
78
  report?: string;
64
79
  error?: string;
65
80
  updatedAt?: number;
@@ -69,6 +84,8 @@ export interface FusionRunTransitionPatch {
69
84
  chainRunId?: string;
70
85
  panelRunId?: string;
71
86
  judgeRunId?: string;
87
+ recovery?: FusionRun["recovery"];
88
+ spawnIntent?: FusionRun["spawnIntent"];
72
89
  report?: string;
73
90
  error?: string;
74
91
  updatedAt?: number;
@@ -105,6 +122,7 @@ export class FusionRunStore {
105
122
  private readonly now: () => number;
106
123
  private readonly idFactory: () => string;
107
124
  private readonly persistence: FusionRunStorePersistence | undefined;
125
+ private restoreError: string | undefined;
108
126
 
109
127
  constructor(options: FusionRunStoreOptions = {}) {
110
128
  this.now = options.now ?? Date.now;
@@ -132,6 +150,11 @@ export class FusionRunStore {
132
150
  return runId ? this.getRunById(runId) : undefined;
133
151
  }
134
152
 
153
+ /** A corrupt newest snapshot must never revive an older active run. */
154
+ getRestoreError(): string | undefined {
155
+ return this.restoreError;
156
+ }
157
+
135
158
  startRun(input: FusionRunStartInput): FusionRun {
136
159
  if (this.activeRun) {
137
160
  throw new FusionRunStoreError(
@@ -162,13 +185,28 @@ export class FusionRunStore {
162
185
  ...(input.operationId !== undefined
163
186
  ? { operationId: input.operationId }
164
187
  : {}),
188
+ ...(input.outputContract !== undefined
189
+ ? { outputContract: input.outputContract }
190
+ : {}),
191
+ ...(input.minimumSuccessfulPanelists !== undefined
192
+ ? { minimumSuccessfulPanelists: input.minimumSuccessfulPanelists }
193
+ : {}),
194
+ ...(input.profileSnapshot !== undefined
195
+ ? { profileSnapshot: cloneProfileSnapshot(input.profileSnapshot) }
196
+ : {}),
197
+ ...(input.timeoutOverrides !== undefined
198
+ ? { timeoutOverrides: { ...input.timeoutOverrides } }
199
+ : {}),
200
+ ...(input.effectiveTimeouts !== undefined
201
+ ? { effectiveTimeouts: { ...input.effectiveTimeouts } }
202
+ : {}),
165
203
  phase: input.phase ?? "chain",
166
204
  createdAt,
167
205
  updatedAt: createdAt,
168
206
  };
207
+ this.persistRun(run);
169
208
  this.activeRun = run;
170
209
  this.rememberRun(run);
171
- this.persistRun(run);
172
210
  return cloneRun(run);
173
211
  }
174
212
 
@@ -216,11 +254,26 @@ export class FusionRunStore {
216
254
  restoreFromEntries(
217
255
  entries: readonly unknown[],
218
256
  ): FusionRunSummary | undefined {
219
- const states = readFusionRunStates(entries);
257
+ this.restoreError = undefined;
220
258
  this.runsById.clear();
221
259
  this.runIdsByOperationId.clear();
222
- for (const state of states) this.rememberRun(state);
223
260
 
261
+ const latestPersisted = lastFusionRunEnvelope(entries);
262
+ if (
263
+ latestPersisted &&
264
+ (!isFusionRunEntry(latestPersisted) || !isFusionRunState(latestPersisted.data))
265
+ ) {
266
+ // History readers may skip malformed entries, but restore must not adopt
267
+ // an older active state after a newer snapshot was corrupted.
268
+ this.activeRun = undefined;
269
+ this.lastRunSummary = undefined;
270
+ this.restoreError =
271
+ "Latest persisted fusion run snapshot is invalid; refusing stale active-run recovery.";
272
+ return undefined;
273
+ }
274
+
275
+ const states = readFusionRunStates(entries);
276
+ for (const state of states) this.rememberRun(state);
224
277
  const latestState = states.at(-1);
225
278
  const summary = readLastFusionRunSummary(entries);
226
279
  this.activeRun =
@@ -341,12 +394,20 @@ function applyPatch(
341
394
  if (patch.judgeObservation !== undefined) {
342
395
  updated.judgeObservation = cloneObservation(patch.judgeObservation);
343
396
  }
397
+ if (patch.completionQuality !== undefined) {
398
+ updated.completionQuality = patch.completionQuality;
399
+ }
344
400
  if (patch.panelOutputs !== undefined) {
345
401
  updated.panelOutputs = clonePanelOutputs(patch.panelOutputs);
346
402
  }
347
403
  if (patch.panelFailures !== undefined) {
348
404
  updated.panelFailures = clonePanelFailures(patch.panelFailures);
349
405
  }
406
+ if (patch.recovery !== undefined) updated.recovery = cloneRecovery(patch.recovery);
407
+ if (patch.spawnIntent === null) delete updated.spawnIntent;
408
+ else if (patch.spawnIntent !== undefined) {
409
+ updated.spawnIntent = cloneSpawnIntent(patch.spawnIntent);
410
+ }
350
411
  if (patch.report !== undefined) updated.report = patch.report;
351
412
  if (patch.error !== undefined) updated.error = patch.error;
352
413
  return updated;
@@ -366,6 +427,10 @@ function applyTransitionPatch(
366
427
  if (patch.chainRunId !== undefined) updated.chainRunId = patch.chainRunId;
367
428
  if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
368
429
  if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
430
+ if (patch.recovery !== undefined) updated.recovery = cloneRecovery(patch.recovery);
431
+ if (patch.spawnIntent !== undefined) {
432
+ updated.spawnIntent = cloneSpawnIntent(patch.spawnIntent);
433
+ }
369
434
  if (patch.report !== undefined) updated.report = patch.report;
370
435
  if (patch.error !== undefined) updated.error = patch.error;
371
436
  return updated;
@@ -379,6 +444,16 @@ function toRunSummary(
379
444
  prompt: run.prompt,
380
445
  profileName: run.profileName,
381
446
  ...(run.operationId !== undefined ? { operationId: run.operationId } : {}),
447
+ ...(run.outputContract !== undefined
448
+ ? { outputContract: run.outputContract }
449
+ : {}),
450
+ ...(run.completionQuality !== undefined
451
+ ? { completionQuality: run.completionQuality }
452
+ : {}),
453
+ ...(run.effectiveTimeouts !== undefined
454
+ ? { effectiveTimeouts: { ...run.effectiveTimeouts } }
455
+ : {}),
456
+ ...(run.recovery !== undefined ? { recovery: cloneRecovery(run.recovery) } : {}),
382
457
  phase: run.phase,
383
458
  createdAt: run.createdAt,
384
459
  updatedAt: run.updatedAt,
@@ -404,6 +479,24 @@ function cloneRun(run: FusionRun): FusionRun {
404
479
  ? { baseProfileName: run.baseProfileName }
405
480
  : {}),
406
481
  ...(run.operationId !== undefined ? { operationId: run.operationId } : {}),
482
+ ...(run.outputContract !== undefined
483
+ ? { outputContract: run.outputContract }
484
+ : {}),
485
+ ...(run.minimumSuccessfulPanelists !== undefined
486
+ ? { minimumSuccessfulPanelists: run.minimumSuccessfulPanelists }
487
+ : {}),
488
+ ...(run.profileSnapshot !== undefined
489
+ ? { profileSnapshot: cloneProfileSnapshot(run.profileSnapshot) }
490
+ : {}),
491
+ ...(run.timeoutOverrides !== undefined
492
+ ? { timeoutOverrides: { ...run.timeoutOverrides } }
493
+ : {}),
494
+ ...(run.effectiveTimeouts !== undefined
495
+ ? { effectiveTimeouts: { ...run.effectiveTimeouts } }
496
+ : {}),
497
+ ...(run.completionQuality !== undefined
498
+ ? { completionQuality: run.completionQuality }
499
+ : {}),
407
500
  phase: run.phase,
408
501
  createdAt: run.createdAt,
409
502
  updatedAt: run.updatedAt,
@@ -434,6 +527,10 @@ function cloneRun(run: FusionRun): FusionRun {
434
527
  ...(run.panelFailures !== undefined
435
528
  ? { panelFailures: clonePanelFailures(run.panelFailures) }
436
529
  : {}),
530
+ ...(run.recovery !== undefined ? { recovery: cloneRecovery(run.recovery) } : {}),
531
+ ...(run.spawnIntent !== undefined
532
+ ? { spawnIntent: cloneSpawnIntent(run.spawnIntent) }
533
+ : {}),
437
534
  ...(run.report !== undefined ? { report: run.report } : {}),
438
535
  ...(run.error !== undefined ? { error: run.error } : {}),
439
536
  };
@@ -443,17 +540,30 @@ function cloneRunSummary(summary: FusionRunSummary): FusionRunSummary {
443
540
  return toRunSummary(summary);
444
541
  }
445
542
 
446
- function isFusionRunEntry(
447
- value: unknown,
448
- ): value is { type: "custom"; customType: string; data: unknown } {
543
+ function lastFusionRunEnvelope(entries: readonly unknown[]): unknown {
544
+ for (let index = entries.length - 1; index >= 0; index--) {
545
+ const entry = entries[index];
546
+ // Locate the newest entry by its envelope before inspecting data. A
547
+ // malformed data field (or its absence) must not revive an older run.
548
+ if (isFusionRunEnvelope(entry)) return entry;
549
+ }
550
+ return undefined;
551
+ }
552
+
553
+ function isFusionRunEnvelope(value: unknown): value is Record<string, unknown> {
449
554
  return (
450
555
  isRecord(value) &&
451
556
  value.type === "custom" &&
452
- value.customType === FUSION_RUN_ENTRY_TYPE &&
453
- "data" in value
557
+ value.customType === FUSION_RUN_ENTRY_TYPE
454
558
  );
455
559
  }
456
560
 
561
+ function isFusionRunEntry(
562
+ value: unknown,
563
+ ): value is { type: "custom"; customType: string; data: unknown } {
564
+ return isFusionRunEnvelope(value) && "data" in value;
565
+ }
566
+
457
567
  function isFusionRunState(value: unknown): value is FusionRun {
458
568
  if (!isRecord(value)) return false;
459
569
  if (!isNonEmptyString(value.id)) return false;
@@ -462,6 +572,53 @@ function isFusionRunState(value: unknown): value is FusionRun {
462
572
  if (value.operationId !== undefined && !isNonEmptyString(value.operationId)) {
463
573
  return false;
464
574
  }
575
+ if (
576
+ value.outputContract !== undefined &&
577
+ value.outputContract !== "plan-review-v1"
578
+ ) {
579
+ return false;
580
+ }
581
+ if (
582
+ value.minimumSuccessfulPanelists !== undefined &&
583
+ value.minimumSuccessfulPanelists !== "majority" &&
584
+ value.minimumSuccessfulPanelists !== "all" &&
585
+ (!isFiniteNumber(value.minimumSuccessfulPanelists) ||
586
+ !Number.isInteger(value.minimumSuccessfulPanelists) ||
587
+ value.minimumSuccessfulPanelists < 1)
588
+ ) {
589
+ return false;
590
+ }
591
+ if (
592
+ value.profileSnapshot !== undefined &&
593
+ !isProfileSnapshot(value.profileSnapshot)
594
+ ) {
595
+ return false;
596
+ }
597
+ // Snapshots are the canonical quorum record for new runs. Older records may
598
+ // also carry the run-level policy, but it must resolve to the same quorum.
599
+ if (
600
+ value.profileSnapshot !== undefined &&
601
+ value.minimumSuccessfulPanelists !== undefined &&
602
+ resolvePersistedQuorum(
603
+ value.minimumSuccessfulPanelists,
604
+ value.profileSnapshot.panel.length,
605
+ ) !== value.profileSnapshot.minimumSuccessfulPanelists
606
+ ) {
607
+ return false;
608
+ }
609
+ if (value.timeoutOverrides !== undefined && !isTimeoutOverrides(value.timeoutOverrides)) {
610
+ return false;
611
+ }
612
+ if (value.effectiveTimeouts !== undefined && !isEffectiveTimeouts(value.effectiveTimeouts)) {
613
+ return false;
614
+ }
615
+ if (
616
+ value.completionQuality !== undefined &&
617
+ value.completionQuality !== "complete" &&
618
+ value.completionQuality !== "partial"
619
+ ) {
620
+ return false;
621
+ }
465
622
  if (
466
623
  value.inlinePanel !== undefined &&
467
624
  (!Array.isArray(value.inlinePanel) ||
@@ -539,19 +696,51 @@ function isFusionRunState(value: unknown): value is FusionRun {
539
696
  ) {
540
697
  return false;
541
698
  }
699
+ if (value.recovery !== undefined && !isRecoveryState(value.recovery)) {
700
+ return false;
701
+ }
702
+ if (value.spawnIntent !== undefined && !isSpawnIntent(value.spawnIntent)) {
703
+ return false;
704
+ }
542
705
  if (value.report !== undefined && typeof value.report !== "string") {
543
706
  return false;
544
707
  }
545
708
  if (value.error !== undefined && typeof value.error !== "string") {
546
709
  return false;
547
710
  }
548
- return true;
711
+ return (
712
+ value.profileSnapshot === undefined ||
713
+ validateFusionRunPanelSlots(value, value.profileSnapshot.panel.length) ===
714
+ undefined
715
+ );
549
716
  }
550
717
 
551
718
  function isFusionRunSummary(value: unknown): value is FusionRunSummary {
552
719
  return isFusionRunState(value) && isTerminalPhase(value.phase);
553
720
  }
554
721
 
722
+ function isTimeoutOverrides(value: unknown): boolean {
723
+ if (!isRecord(value)) return false;
724
+ return [
725
+ value.panelistTimeoutMs,
726
+ value.panelTimeoutMs,
727
+ value.panelGraceMs,
728
+ value.judgeTimeoutMs,
729
+ ].every((timeout) => timeout === undefined || (isFiniteNumber(timeout) && Number.isInteger(timeout) && timeout > 0));
730
+ }
731
+
732
+ function isEffectiveTimeouts(value: unknown): boolean {
733
+ return (
734
+ isTimeoutOverrides(value) &&
735
+ isRecord(value) &&
736
+ isFiniteNumber(value.panelistTimeoutMs) &&
737
+ isFiniteNumber(value.panelTimeoutMs) &&
738
+ isFiniteNumber(value.panelGraceMs) &&
739
+ isFiniteNumber(value.judgeTimeoutMs) &&
740
+ typeof value.usesLegacyTimeout === "boolean"
741
+ );
742
+ }
743
+
555
744
  function isFusionPhase(value: unknown): value is FusionPhase {
556
745
  return (
557
746
  value === "panel" ||
@@ -567,6 +756,45 @@ function isTerminalPhase(value: unknown): value is FusionTerminalPhase {
567
756
  return value === "done" || value === "failed" || value === "cancelled";
568
757
  }
569
758
 
759
+ /**
760
+ * Checks persisted slot references before they are restored or merged. Legacy
761
+ * runs omit a profile snapshot, so their caller supplies the safely resolved
762
+ * profile length during restore.
763
+ */
764
+ export function validateFusionRunPanelSlots(
765
+ run: Pick<
766
+ FusionRun,
767
+ "panelOutputs" | "panelFailures" | "panelStoppedIndices" | "recovery"
768
+ >,
769
+ panelLength: number,
770
+ ): string | undefined {
771
+ const slotGroups: ReadonlyArray<readonly number[] | undefined> = [
772
+ run.panelOutputs?.map((output) => output.index),
773
+ run.panelFailures?.map((failure) => failure.index),
774
+ run.panelStoppedIndices,
775
+ run.recovery?.failedPanelIndices,
776
+ ];
777
+ for (const slots of slotGroups) {
778
+ for (const slot of slots ?? []) {
779
+ if (!isPanelSlotIndex(slot) || slot >= panelLength) {
780
+ return `Persisted panel slot ${String(slot)} is outside the configured panel.`;
781
+ }
782
+ }
783
+ }
784
+ return undefined;
785
+ }
786
+
787
+ function resolvePersistedQuorum(
788
+ policy: NonNullable<FusionRun["minimumSuccessfulPanelists"]>,
789
+ panelLength: number,
790
+ ): number {
791
+ if (policy === "all") return panelLength;
792
+ if (typeof policy === "number") {
793
+ return panelLength > 1 ? Math.max(2, Math.min(policy, panelLength)) : 1;
794
+ }
795
+ return Math.ceil(panelLength / 2);
796
+ }
797
+
570
798
  function isPanelOutputArray(
571
799
  value: unknown,
572
800
  ): value is NonNullable<FusionRun["panelOutputs"]> {
@@ -578,7 +806,7 @@ function isPanelOutput(
578
806
  ): value is NonNullable<FusionRun["panelOutputs"]>[number] {
579
807
  return (
580
808
  isRecord(value) &&
581
- isFiniteNumber(value.index) &&
809
+ isPanelSlotIndex(value.index) &&
582
810
  isNonEmptyString(value.agent) &&
583
811
  typeof value.output === "string" &&
584
812
  (value.id === undefined || typeof value.id === "string") &&
@@ -605,7 +833,7 @@ function isPanelFailure(
605
833
  ): value is NonNullable<FusionRun["panelFailures"]>[number] {
606
834
  return (
607
835
  isRecord(value) &&
608
- isFiniteNumber(value.index) &&
836
+ isPanelSlotIndex(value.index) &&
609
837
  isNonEmptyString(value.agent) &&
610
838
  typeof value.summary === "string" &&
611
839
  (value.id === undefined || typeof value.id === "string") &&
@@ -621,6 +849,144 @@ function isPanelFailure(
621
849
  );
622
850
  }
623
851
 
852
+ function isProfileSnapshot(value: unknown): value is FusionProfileSnapshot {
853
+ if (!isRecord(value) || !Array.isArray(value.panel) || value.panel.length === 0) {
854
+ return false;
855
+ }
856
+ if (!value.panel.every(isSnapshotPanelMember) || !isSnapshotJudge(value.judge)) {
857
+ return false;
858
+ }
859
+ if (
860
+ !isFiniteNumber(value.minimumSuccessfulPanelists) ||
861
+ !Number.isInteger(value.minimumSuccessfulPanelists) ||
862
+ value.minimumSuccessfulPanelists < 1 ||
863
+ value.minimumSuccessfulPanelists > value.panel.length
864
+ ) {
865
+ return false;
866
+ }
867
+ if (value.context !== undefined && value.context !== "fresh" && value.context !== "fork") {
868
+ return false;
869
+ }
870
+ if (value.stopWhenPanelAgrees !== undefined && typeof value.stopWhenPanelAgrees !== "boolean") {
871
+ return false;
872
+ }
873
+ if (value.blindPanelLabels !== undefined && typeof value.blindPanelLabels !== "boolean") {
874
+ return false;
875
+ }
876
+ if (value.synthesis !== undefined && value.synthesis !== "select" && value.synthesis !== "merge") {
877
+ return false;
878
+ }
879
+ return value.judgeToolBudget === undefined || isSnapshotToolBudget(value.judgeToolBudget);
880
+ }
881
+
882
+ function isSnapshotPanelMember(value: unknown): boolean {
883
+ if (!isRecord(value) || !isNonEmptyString(value.id) || !isSnapshotAgent(value.agent)) {
884
+ return false;
885
+ }
886
+ return (
887
+ (value.label === undefined || isNonEmptyString(value.label)) &&
888
+ (value.model === undefined || isNonEmptyString(value.model)) &&
889
+ (value.thinking === undefined || isThinkingLevel(value.thinking)) &&
890
+ (value.role === undefined || typeof value.role === "string") &&
891
+ (value.question === undefined || isNonEmptyString(value.question))
892
+ );
893
+ }
894
+
895
+ function isSnapshotJudge(value: unknown): boolean {
896
+ return (
897
+ isRecord(value) &&
898
+ isSnapshotAgent(value.agent) &&
899
+ (value.model === undefined || isNonEmptyString(value.model)) &&
900
+ (value.thinking === undefined || isThinkingLevel(value.thinking))
901
+ );
902
+ }
903
+
904
+ function isSnapshotAgent(value: unknown): boolean {
905
+ return (
906
+ isNonEmptyString(value) &&
907
+ /^[^\s.]+(?:\.[^\s.]+)*$/.test(value.trim())
908
+ );
909
+ }
910
+
911
+ function isThinkingLevel(value: unknown): boolean {
912
+ return value === "off" || value === "minimal" || value === "low" ||
913
+ value === "medium" || value === "high" || value === "xhigh";
914
+ }
915
+
916
+ function isSnapshotToolBudget(value: unknown): boolean {
917
+ if (!isRecord(value) || (value.soft === undefined && value.hard === undefined)) {
918
+ return false;
919
+ }
920
+ if (value.soft !== undefined && (!isFiniteNumber(value.soft) || !Number.isInteger(value.soft) || value.soft < 1)) {
921
+ return false;
922
+ }
923
+ if (value.hard !== undefined && (!isFiniteNumber(value.hard) || !Number.isInteger(value.hard) || value.hard < 1)) {
924
+ return false;
925
+ }
926
+ if (typeof value.soft === "number" && typeof value.hard === "number" && value.soft > value.hard) {
927
+ return false;
928
+ }
929
+ return value.block === undefined || value.block === "*" ||
930
+ (Array.isArray(value.block) && value.block.length > 0 && value.block.every(isNonEmptyString));
931
+ }
932
+
933
+ function cloneProfileSnapshot(snapshot: FusionProfileSnapshot): FusionProfileSnapshot {
934
+ return {
935
+ panel: snapshot.panel.map((member) => ({ ...member })),
936
+ judge: { ...snapshot.judge },
937
+ minimumSuccessfulPanelists: snapshot.minimumSuccessfulPanelists,
938
+ ...(snapshot.context !== undefined ? { context: snapshot.context } : {}),
939
+ ...(snapshot.stopWhenPanelAgrees !== undefined
940
+ ? { stopWhenPanelAgrees: snapshot.stopWhenPanelAgrees }
941
+ : {}),
942
+ ...(snapshot.blindPanelLabels !== undefined
943
+ ? { blindPanelLabels: snapshot.blindPanelLabels }
944
+ : {}),
945
+ ...(snapshot.judgeToolBudget !== undefined
946
+ ? {
947
+ judgeToolBudget: {
948
+ ...snapshot.judgeToolBudget,
949
+ ...(Array.isArray(snapshot.judgeToolBudget.block)
950
+ ? { block: [...snapshot.judgeToolBudget.block] }
951
+ : {}),
952
+ },
953
+ }
954
+ : {}),
955
+ ...(snapshot.synthesis !== undefined ? { synthesis: snapshot.synthesis } : {}),
956
+ };
957
+ }
958
+
959
+ function isRecoveryState(value: unknown): value is FusionRecoveryState {
960
+ return (
961
+ isRecord(value) &&
962
+ value.retryDeferred === true &&
963
+ Array.isArray(value.failedPanelIndices) &&
964
+ value.failedPanelIndices.every(isPanelSlotIndex)
965
+ );
966
+ }
967
+
968
+ function isPanelSlotIndex(value: unknown): value is number {
969
+ return isFiniteNumber(value) && Number.isInteger(value) && value >= 0;
970
+ }
971
+
972
+ function cloneRecovery(recovery: FusionRecoveryState): FusionRecoveryState {
973
+ return { ...recovery, failedPanelIndices: [...recovery.failedPanelIndices] };
974
+ }
975
+
976
+ function isSpawnIntent(value: unknown): value is FusionRun["spawnIntent"] {
977
+ return (
978
+ isRecord(value) &&
979
+ (value.stage === "panel" || value.stage === "judge") &&
980
+ isFiniteNumber(value.requestedAt)
981
+ );
982
+ }
983
+
984
+ function cloneSpawnIntent(
985
+ intent: NonNullable<FusionRun["spawnIntent"]>,
986
+ ): NonNullable<FusionRun["spawnIntent"]> {
987
+ return { ...intent };
988
+ }
989
+
624
990
  function clonePanelOutputs(
625
991
  outputs: NonNullable<FusionRun["panelOutputs"]>,
626
992
  ): NonNullable<FusionRun["panelOutputs"]> {
package/src/status.ts CHANGED
@@ -52,7 +52,7 @@ export function formatFusionStatusText(
52
52
  run.phase === "judge" ? run.judgeRunId : (run.chainRunId ?? run.panelRunId);
53
53
  const phase = phaseLabel ?? run.phase;
54
54
  if (progress) {
55
- return `fusion: panel · ${formatProgressCounts(progress)} · ${run.profileName}`;
55
+ return `fusion: ${phase} · ${formatProgressCounts(progress)} · ${run.profileName}`;
56
56
  }
57
57
  if (activeRunId) {
58
58
  return `fusion: ${phase} · ${run.profileName} · ${activeRunId}`;