@hassangameryt/freeflow 0.7.2 → 0.7.3

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.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/README.md +147 -127
  6. package/capabilities/cognitive-routing/SKILL.md +162 -93
  7. package/gemini-extension.json +1 -1
  8. package/package.json +1 -1
  9. package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
  10. package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
  11. package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
  12. package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
  13. package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
  14. package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
  15. package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
  16. package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
  17. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  18. package/pi-extension/dist/freeflow-context/tool.js +7 -6
  19. package/pi-extension/dist/index.js +36 -15
  20. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  21. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  22. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  23. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  24. package/pi-extension/dist/provider-support/index.js +4 -0
  25. package/pi-extension/dist/runtime/request-history.js +216 -0
  26. package/pi-extension/dist/runtime/runtime-context.js +29 -10
  27. package/pi-extension/dist/session-sources/events.js +22 -0
  28. package/pi-extension/dist/session-sources/provenance.js +32 -0
  29. package/pi-extension/dist/session-sources/sources.js +51 -7
  30. package/pi-extension/dist/settings/settings-ui.js +247 -47
  31. package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
  32. package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
  33. package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
  34. package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
  35. package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
  36. package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
  37. package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
  38. package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
  39. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  40. package/pi-extension/src/freeflow-context/tool.ts +7 -6
  41. package/pi-extension/src/index.ts +36 -14
  42. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  43. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  44. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  45. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  46. package/pi-extension/src/provider-support/index.ts +5 -0
  47. package/pi-extension/src/runtime/request-history.ts +243 -0
  48. package/pi-extension/src/runtime/runtime-context.ts +32 -10
  49. package/pi-extension/src/session-sources/events.ts +22 -0
  50. package/pi-extension/src/session-sources/provenance.ts +39 -0
  51. package/pi-extension/src/session-sources/sources.ts +77 -6
  52. package/pi-extension/src/settings/settings-ui.ts +276 -46
  53. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  54. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  55. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
  57. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
  59. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
  60. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
  61. package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
  62. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
  63. package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
  64. package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
  65. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  66. package/pi-extension/tests/fixtures/routing-native.js +4 -1
  67. package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
  68. package/pi-extension/tests/integration/pi-extension.test.js +508 -29
  69. package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
  70. package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
  71. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  72. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  73. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  74. package/pi-extension/tests/provider-support/native.test.js +183 -0
  75. package/plugin.json +1 -1
  76. package/runtime/prompts/cognitive-routing.md +4 -4
  77. package/skills/design-for-depth/SKILL.md +1 -1
  78. package/skills/design-for-depth/references/implementation-decisions.md +3 -3
  79. package/skills/discuss/SKILL.md +2 -2
@@ -22,10 +22,12 @@ import {
22
22
  import { isPiFlowHost } from "../runtime/runtime-identity.js";
23
23
  import type {
24
24
  CognitiveRoutingCapabilityState,
25
+ CognitiveRoutingDelegationMode,
25
26
  CognitiveRoutingProfile,
26
27
  CognitiveRoutingProfileName,
27
28
  CognitiveRoutingThinkingLevel,
28
29
  } from "../cognitive-routing-v2/config.js";
30
+ import { isWorkerProfile, workersForDelegation } from "../cognitive-routing-v2/types.js";
29
31
 
30
32
  const DEFAULT_FREEFLOW_ENABLED = true;
31
33
  const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
@@ -67,13 +69,26 @@ type SettingsItem = {
67
69
  transient?: boolean;
68
70
  };
69
71
 
72
+ type SessionRoutingPair = {
73
+ provider: string;
74
+ modelId: string;
75
+ thinking: CognitiveRoutingThinkingLevel;
76
+ };
77
+
70
78
  type CognitiveRoutingSettingsController = {
71
79
  state(): { effective: boolean; controlMode: string; activeProfile?: string };
80
+ sessionProfileOverrides(): Partial<Record<CognitiveRoutingProfileName, SessionRoutingPair>>;
72
81
  setManualProfile(
73
- profile: "executor" | "coordinator",
82
+ profile: CognitiveRoutingProfileName,
74
83
  mechanism?: string,
75
84
  ): Promise<{ status: string; reason?: string }>;
76
85
  setAutomaticControl(mechanism?: string): Promise<{ status: string; reason?: string }>;
86
+ setSessionProfileOverride(
87
+ profile: CognitiveRoutingProfileName,
88
+ override: SessionRoutingPair | null,
89
+ mechanism?: string,
90
+ ): Promise<{ status: string; reason?: string }>;
91
+ resetSessionProfileOverrides(mechanism?: string): Promise<{ status: string; reason?: string }>;
77
92
  };
78
93
 
79
94
  type AfterChangeOptions = {
@@ -293,6 +308,79 @@ function createScopedBooleanItem(options: ScopedBooleanItemOptions): SettingsIte
293
308
  return item;
294
309
  }
295
310
 
311
+ function createScopedDelegationItem(options: {
312
+ scope: Exclude<ConfigScope, "session">;
313
+ rawConfig: Record<string, unknown>;
314
+ localConfig: Record<string, unknown>;
315
+ effectiveValue: CognitiveRoutingDelegationMode;
316
+ effectiveSource: ConfigSource;
317
+ }): SettingsItem {
318
+ const path = ["cognitiveRouting", "delegation"];
319
+ const repositoryValue = getPath(options.rawConfig, path);
320
+ const inheritedValue: CognitiveRoutingDelegationMode = ["executor", "helper", "both"].includes(
321
+ repositoryValue as string,
322
+ )
323
+ ? (repositoryValue as CognitiveRoutingDelegationMode)
324
+ : "executor";
325
+ const inheritedSource: ConfigSource = repositoryValue === undefined ? "builtin" : "repository";
326
+ const localValue = getPath(options.localConfig, path);
327
+ const descriptions = {
328
+ executor: "Coordinator delegates assignments to Executor.",
329
+ helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
330
+ both: "Coordinator chooses Helper or Executor for each assignment.",
331
+ };
332
+ const item: SettingsItem =
333
+ options.scope === "local"
334
+ ? {
335
+ id: "freeflow.cognitiveRouting.delegation",
336
+ label: "Delegation method",
337
+ description:
338
+ "Choose enabled workers for automatic routing. Inherit uses the repository value; this setting does not rank model presets.",
339
+ path,
340
+ kind: "enum",
341
+ value: ["executor", "helper", "both"].includes(localValue as string) ? localValue : LOCAL_INHERIT,
342
+ values: [LOCAL_INHERIT, "executor", "helper", "both"],
343
+ valueLabels: {
344
+ inherit: "Inherit repository",
345
+ executor: "Executor only",
346
+ helper: "Helper only",
347
+ both: "Helper and Executor",
348
+ },
349
+ valueDescriptions: {
350
+ inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
351
+ ...descriptions,
352
+ },
353
+ configScope: "local",
354
+ configValues: { inherit: undefined, executor: "executor", helper: "helper", both: "both" },
355
+ effectiveValue: options.effectiveValue,
356
+ effectiveSource: options.effectiveSource,
357
+ inheritedValue,
358
+ inheritedSource,
359
+ }
360
+ : {
361
+ id: "freeflow.cognitiveRouting.delegation",
362
+ label: "Delegation method",
363
+ description: "Choose enabled workers for automatic routing. This edits shared .freeflow/config.json.",
364
+ path,
365
+ kind: "enum",
366
+ value: inheritedValue,
367
+ values: ["executor", "helper", "both"],
368
+ valueLabels: {
369
+ executor: "Executor only",
370
+ helper: "Helper only",
371
+ both: "Helper and Executor",
372
+ },
373
+ valueDescriptions: descriptions,
374
+ defaultValue: "executor",
375
+ configScope: "repository",
376
+ effectiveValue: options.effectiveValue,
377
+ effectiveSource: options.effectiveSource,
378
+ localOverrideValue: ["executor", "helper", "both"].includes(localValue as string) ? localValue : undefined,
379
+ };
380
+ item.displaySuffix = coreDisplaySuffix(item);
381
+ return item;
382
+ }
383
+
296
384
  function resolveSettingsCoreView(
297
385
  rawConfig: Record<string, unknown>,
298
386
  layers?: Awaited<ReturnType<typeof readFreeflowConfigLayers>>,
@@ -371,6 +459,7 @@ function createSessionBooleanItem(options: {
371
459
  function sessionFreeflowItems(
372
460
  state: Awaited<ReturnType<typeof readCapabilityState>>,
373
461
  cognitiveRoutingController?: CognitiveRoutingSettingsController,
462
+ ctx?: any,
374
463
  ): SettingsItem[] {
375
464
  const sessionOverrides = state.sessionOverrides as Record<string, boolean>;
376
465
  const configured = state.configuredCoreConfig;
@@ -424,48 +513,79 @@ function sessionFreeflowItems(
424
513
  conversationHistoryItem.inactive = freeflowInactive;
425
514
 
426
515
  const cognitiveRoutingState = cognitiveRoutingController?.state();
516
+ const delegation = state.cognitiveRouting?.delegation ?? "executor";
517
+ const heldProfile = cognitiveRoutingState?.controlMode.startsWith("manual-")
518
+ ? cognitiveRoutingState.activeProfile
519
+ : undefined;
520
+ const availableProfiles: CognitiveRoutingProfileName[] = ["coordinator", ...workersForDelegation(delegation)];
521
+ if (heldProfile === "coordinator" || isWorkerProfile(heldProfile)) availableProfiles.push(heldProfile);
522
+ const manualProfiles = [...new Set(availableProfiles)];
427
523
  const cognitiveRoutingProfile =
428
- cognitiveRoutingState?.controlMode === "manual-executor"
429
- ? "executor"
430
- : cognitiveRoutingState?.controlMode === "manual-coordinator"
431
- ? "coordinator"
432
- : "auto";
524
+ heldProfile && manualProfiles.includes(heldProfile as CognitiveRoutingProfileName) ? heldProfile : "auto";
433
525
  const cognitiveRoutingItem: SettingsItem | undefined = state.cognitiveRouting
434
526
  ? {
435
527
  id: "freeflow.cognitiveRouting.profile",
436
528
  label: "Cognitive Routing",
437
- description: "Hold a profile manually or release the hold for automatic model control.",
529
+ description: "Hold an enabled profile manually or release the hold for automatic model control.",
438
530
  kind: "enum",
439
531
  value: cognitiveRoutingProfile,
440
- values: ["auto", "executor", "coordinator"],
441
- valueLabels: {
442
- auto: "automatic",
443
- executor: "manual · executor",
444
- coordinator: "manual · coordinator",
445
- },
446
- valueDescriptions: {
447
- auto: "Release the manual hold and return to Coordinator reconciliation.",
448
- executor: "Hold the executor profile until /freeflow profile auto.",
449
- coordinator: "Hold the coordinator profile until /freeflow profile auto.",
450
- },
532
+ values: ["auto", ...manualProfiles],
533
+ valueLabels: Object.fromEntries([
534
+ ["auto", "automatic"],
535
+ ...manualProfiles.map((profile) => [profile, `manual · ${profile}`]),
536
+ ]),
537
+ valueDescriptions: Object.fromEntries([
538
+ ["auto", "Release the manual hold and return to Coordinator reconciliation."],
539
+ ...manualProfiles.map((profile) => [profile, `Hold the ${profile} profile until /freeflow profile auto.`]),
540
+ ]),
451
541
  inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
452
542
  runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
453
543
  displaySuffix: cognitiveRoutingState?.effective ? cognitiveRoutingProfile : "unavailable",
454
544
  }
455
545
  : undefined;
456
546
 
547
+ const sessionProfiles = cognitiveRoutingController?.sessionProfileOverrides();
548
+ const profileItems =
549
+ state.cognitiveRouting && cognitiveRoutingController
550
+ ? manualProfiles.map((name) =>
551
+ cognitiveRoutingProfileItem({
552
+ name,
553
+ scope: "session",
554
+ rawConfig: {},
555
+ localConfig: {},
556
+ capabilityState: state.cognitiveRouting as CognitiveRoutingCapabilityState,
557
+ ctx,
558
+ sessionProfiles,
559
+ }),
560
+ )
561
+ : [];
562
+ const cognitiveRoutingPresets: SettingsItem | undefined = profileItems.length
563
+ ? {
564
+ id: "freeflow.cognitiveRouting.presets",
565
+ label: "Cognitive Routing presets",
566
+ description: "Temporarily choose complete model/effort pairs for profiles enabled in this Pi session.",
567
+ kind: "group",
568
+ value: profileItems.some((item) => item.value !== LOCAL_INHERIT && item.value !== undefined),
569
+ displaySuffix: `${profileItems.filter((item) => item.value !== LOCAL_INHERIT && item.value !== undefined).length}/${profileItems.length} session overrides`,
570
+ inactive: freeflowInactive || !state.cognitiveRouting?.effective || cognitiveRoutingController === undefined,
571
+ children: profileItems,
572
+ }
573
+ : undefined;
574
+
457
575
  return [
458
576
  freeflowItem,
459
577
  ...(cognitiveRoutingItem ? [cognitiveRoutingItem] : []),
578
+ ...(cognitiveRoutingPresets ? [cognitiveRoutingPresets] : []),
460
579
  {
461
580
  id: "freeflow.session.reset",
462
581
  label: "Reset session overrides",
463
- description: "Clear Freeflow, Context Virtualization, and Conversation History overrides for this Pi session.",
582
+ description:
583
+ "Clear Freeflow, Context Virtualization, Conversation History, and routing profile overrides for this Pi session.",
464
584
  kind: "enum",
465
585
  value: "available",
466
586
  values: ["reset"],
467
587
  valueLabels: { reset: "Reset all session overrides" },
468
- valueDescriptions: { reset: "Return every session setting to its configured value." },
588
+ valueDescriptions: { reset: "Return every session setting and routing preset to its configured value." },
469
589
  format: () => "available",
470
590
  transient: true,
471
591
  },
@@ -574,6 +694,7 @@ function cognitiveRoutingProfileDisplay(value: unknown): string {
574
694
  }
575
695
 
576
696
  function cognitiveRoutingSettingsSource(source: unknown): ConfigSource {
697
+ if (source === "session") return "session";
577
698
  if (source === "repository") return "repository";
578
699
  if (source === "personal" || source === "local") return "local";
579
700
  return "builtin";
@@ -581,6 +702,7 @@ function cognitiveRoutingSettingsSource(source: unknown): ConfigSource {
581
702
 
582
703
  function cognitiveRoutingProfileDisplaySuffix(source: unknown, scope: ConfigScope): string | undefined {
583
704
  if (!source) return undefined;
705
+ if (source === "session") return "(session override)";
584
706
  if (source === "personal" && scope === "repository") return "(effective personal)";
585
707
  return `(${source})`;
586
708
  }
@@ -605,6 +727,11 @@ function createCognitiveRoutingProfileWizard(
605
727
  currentValue: unknown,
606
728
  models: CognitiveRoutingModelOption[],
607
729
  allowInherit: boolean,
730
+ inheritChoice: { label: string; description: string; summary: string } = {
731
+ label: "Inherit repository preset",
732
+ description: "Remove the personal override without changing the repository preset.",
733
+ summary: "inherit repository preset",
734
+ },
608
735
  ): SettingsWizard {
609
736
  const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
610
737
  const currentModelKey = currentProfile
@@ -616,8 +743,8 @@ function createCognitiveRoutingProfileWizard(
616
743
  {
617
744
  key: LOCAL_INHERIT,
618
745
  value: LOCAL_INHERIT,
619
- label: "Inherit repository preset",
620
- description: "Remove the personal override without changing the repository preset.",
746
+ label: inheritChoice.label,
747
+ description: inheritChoice.description,
621
748
  },
622
749
  ]
623
750
  : []),
@@ -652,7 +779,7 @@ function createCognitiveRoutingProfileWizard(
652
779
  if (selectedValues.length === 1) {
653
780
  const selected = selectedValues[0];
654
781
  if (selected === LOCAL_INHERIT) {
655
- return cognitiveRoutingConfirmStep("inherit repository preset");
782
+ return cognitiveRoutingConfirmStep(inheritChoice.summary);
656
783
  }
657
784
  const model = selected as CognitiveRoutingModelOption;
658
785
  if (!model?.key || model.thinkingLevels.length === 0) return undefined;
@@ -695,36 +822,65 @@ function cognitiveRoutingProfileItem(options: {
695
822
  localConfig: Record<string, unknown>;
696
823
  capabilityState: CognitiveRoutingCapabilityState | undefined;
697
824
  ctx: any;
825
+ sessionProfiles?: Partial<Record<CognitiveRoutingProfileName, SessionRoutingPair>>;
698
826
  }): SettingsItem {
699
827
  const path = ["cognitiveRouting", "profiles", options.name];
700
828
  const repositoryProfile = getPath(options.rawConfig, path);
701
829
  const localProfile = getPath(options.localConfig, path);
702
- const effectiveProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
830
+ const configuredProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
831
+ const sessionPair = options.sessionProfiles?.[options.name];
832
+ const sessionProfile = sessionPair
833
+ ? { provider: sessionPair.provider, model: sessionPair.modelId, thinking: sessionPair.thinking }
834
+ : undefined;
835
+ const effectiveProfile = sessionProfile ?? configuredProfile;
703
836
  const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
704
- const value = options.scope === "local" ? localValue : repositoryProfile;
705
- const source =
837
+ const value =
838
+ options.scope === "session"
839
+ ? (sessionProfile ?? LOCAL_INHERIT)
840
+ : options.scope === "local"
841
+ ? localValue
842
+ : repositoryProfile;
843
+ const configuredSource =
706
844
  options.capabilityState?.profileSources?.[options.name] ??
707
845
  (isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
846
+ const source = options.scope === "session" ? (sessionProfile ? "session" : configuredSource) : configuredSource;
708
847
  const models = cognitiveRoutingModelOptions(options.ctx);
709
- const inactive = options.scope === "local" && models.length === 0 && localValue !== LOCAL_INHERIT;
848
+ const inactive = options.scope !== "repository" && models.length === 0 && value !== LOCAL_INHERIT;
849
+ const inheritChoice =
850
+ options.scope === "session"
851
+ ? {
852
+ label: "Inherit configured preset",
853
+ description: "Remove the session override without changing local or repository configuration.",
854
+ summary: "inherit configured preset",
855
+ }
856
+ : undefined;
710
857
 
711
858
  return {
712
859
  id: `freeflow.cognitiveRouting.${options.name}`,
713
860
  label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
714
861
  description:
715
- "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
862
+ options.scope === "session"
863
+ ? "Temporarily choose the model and effort for this profile in the current Pi session only."
864
+ : "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
716
865
  path,
717
866
  kind: "string",
718
867
  value,
719
- format: cognitiveRoutingProfileDisplay,
868
+ format: (current) => {
869
+ const inherits = current === LOCAL_INHERIT || (options.scope === "session" && current === undefined);
870
+ return inherits
871
+ ? options.scope === "session"
872
+ ? "inherit configured preset"
873
+ : "inherit repository preset"
874
+ : cognitiveRoutingProfileDisplay(current);
875
+ },
720
876
  configScope: options.scope,
721
877
  effectiveValue: effectiveProfile,
722
878
  effectiveSource: cognitiveRoutingSettingsSource(source),
723
- inheritedValue: repositoryProfile,
724
- inheritedSource: "repository",
879
+ inheritedValue: options.scope === "session" ? configuredProfile : repositoryProfile,
880
+ inheritedSource: cognitiveRoutingSettingsSource(configuredSource ?? "builtin"),
725
881
  inactive,
726
882
  displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
727
- wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope === "local"),
883
+ wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope !== "repository", inheritChoice),
728
884
  };
729
885
  }
730
886
 
@@ -747,7 +903,7 @@ function freeflowItems(
747
903
  runtimeAvailable?: boolean;
748
904
  } = {},
749
905
  ): SettingsItem[] {
750
- const scope = options.scope ?? "repository";
906
+ const scope: Exclude<ConfigScope, "session"> = options.scope === "local" ? "local" : "repository";
751
907
  const layers = options.layers;
752
908
  const { localConfig, core, sources } = resolveSettingsCoreView(rawConfig, layers);
753
909
 
@@ -812,6 +968,16 @@ function freeflowItems(
812
968
  cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
813
969
  cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
814
970
 
971
+ const cognitiveRoutingDelegationItem = createScopedDelegationItem({
972
+ scope,
973
+ rawConfig,
974
+ localConfig,
975
+ effectiveValue: cognitiveRoutingState?.delegation ?? "executor",
976
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.delegationSource),
977
+ });
978
+ cognitiveRoutingDelegationItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
979
+ cognitiveRoutingDelegationItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
980
+
815
981
  const cognitiveRoutingProjectionItem = createScopedBooleanItem({
816
982
  scope,
817
983
  rawConfig,
@@ -819,7 +985,7 @@ function freeflowItems(
819
985
  id: "freeflow.cognitiveRouting.projection",
820
986
  label: "Context projection",
821
987
  description:
822
- "Select Executor evidence for Coordinator; disable projection to keep ordinary Pi context in both profiles.",
988
+ "Select worker evidence for Coordinator; disable projection to keep ordinary Pi context in all enabled profiles.",
823
989
  path: ["cognitiveRouting", "projection"],
824
990
  effectiveValue: cognitiveRoutingState?.projection ?? false,
825
991
  effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
@@ -828,7 +994,7 @@ function freeflowItems(
828
994
  cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
829
995
  cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
830
996
 
831
- const cognitiveRoutingProfiles = ["executor", "coordinator"].map((name) =>
997
+ const cognitiveRoutingProfiles = ["coordinator", "helper", "executor"].map((name) =>
832
998
  cognitiveRoutingProfileItem({
833
999
  name: name as CognitiveRoutingProfileName,
834
1000
  scope,
@@ -856,12 +1022,17 @@ function freeflowItems(
856
1022
  label: "Cognitive Routing",
857
1023
  description: cognitiveRoutingRuntimeDisabled
858
1024
  ? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
859
- : "Configure the Executor and Coordinator profiles and choose whether Freeflow may manage model state for this repository.",
1025
+ : "Configure Coordinator, Helper, and Executor profiles plus the workers available for delegation.",
860
1026
  kind: "group" as const,
861
1027
  value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
862
1028
  inactive: freeflowInactive,
863
1029
  displaySuffix: cognitiveRoutingStatus,
864
- children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, cognitiveRoutingProjectionItem],
1030
+ children: [
1031
+ cognitiveRoutingEnabledItem,
1032
+ cognitiveRoutingDelegationItem,
1033
+ ...cognitiveRoutingProfiles,
1034
+ cognitiveRoutingProjectionItem,
1035
+ ],
865
1036
  } satisfies SettingsItem;
866
1037
  })();
867
1038
 
@@ -885,6 +1056,7 @@ function pruneKnownDefaults(config: Record<string, unknown>) {
885
1056
  { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
886
1057
  { path: ["contextVirtualization"], value: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED },
887
1058
  { path: ["conversationHistory"], value: DEFAULT_CONVERSATION_HISTORY_ENABLED },
1059
+ { path: ["cognitiveRouting", "delegation"], value: "executor" },
888
1060
  ];
889
1061
 
890
1062
  for (const item of defaultPaths) {
@@ -971,7 +1143,7 @@ async function updateConfig(
971
1143
  }
972
1144
 
973
1145
  function isCognitiveRoutingProfileItem(item: SettingsItem): boolean {
974
- return item.id === "freeflow.cognitiveRouting.executor" || item.id === "freeflow.cognitiveRouting.coordinator";
1146
+ return ["coordinator", "helper", "executor"].some((profile) => item.id === `freeflow.cognitiveRouting.${profile}`);
975
1147
  }
976
1148
 
977
1149
  function valueForDisplay(item: SettingsItem): string {
@@ -1015,10 +1187,13 @@ function refreshSettingsDerivedState(items: SettingsItem[]) {
1015
1187
  const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
1016
1188
  const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
1017
1189
  const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
1018
- const cognitiveRoutingProfiles = [
1019
- findSettingsItem(items, "freeflow.cognitiveRouting.executor"),
1020
- findSettingsItem(items, "freeflow.cognitiveRouting.coordinator"),
1021
- ];
1190
+ const cognitiveRoutingDelegationItem = findSettingsItem(items, "freeflow.cognitiveRouting.delegation");
1191
+ const delegation = (
1192
+ cognitiveRoutingDelegationItem ? effectiveItemValue(cognitiveRoutingDelegationItem) : "executor"
1193
+ ) as CognitiveRoutingDelegationMode;
1194
+ const cognitiveRoutingProfiles = ["coordinator", ...workersForDelegation(delegation)].map((profile) =>
1195
+ findSettingsItem(items, `freeflow.cognitiveRouting.${profile}`),
1196
+ );
1022
1197
 
1023
1198
  if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
1024
1199
  const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
@@ -1336,7 +1511,7 @@ export async function handleFreeflowCommand(
1336
1511
  let reconcileCognitiveRouting = settingsScope === "session";
1337
1512
  const items =
1338
1513
  settingsScope === "session"
1339
- ? sessionFreeflowItems(state, cognitiveRoutingController)
1514
+ ? sessionFreeflowItems(state, cognitiveRoutingController, ctx)
1340
1515
  : freeflowItems(raw, {
1341
1516
  scope: settingsScope,
1342
1517
  layers,
@@ -1368,7 +1543,7 @@ export async function handleFreeflowCommand(
1368
1543
  value === "auto"
1369
1544
  ? await cognitiveRoutingController.setAutomaticControl("profile-settings")
1370
1545
  : await cognitiveRoutingController.setManualProfile(
1371
- value as "executor" | "coordinator",
1546
+ value as CognitiveRoutingProfileName,
1372
1547
  "profile-settings",
1373
1548
  );
1374
1549
  if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
@@ -1382,8 +1557,63 @@ export async function handleFreeflowCommand(
1382
1557
  return { changed: true, reloadRequired: false };
1383
1558
  }
1384
1559
  if (item.id === "freeflow.session.reset") {
1385
- const result = await resetSessionOverrides(ctx, pi);
1386
- return { changed: result.changed, reloadRequired: result.reloadRequired };
1560
+ const routingResult = cognitiveRoutingController
1561
+ ? await cognitiveRoutingController.resetSessionProfileOverrides("Reset session overrides")
1562
+ : { status: "unchanged" };
1563
+ if (routingResult.status === "blocked" || routingResult.reason) {
1564
+ ctx.ui.notify(
1565
+ `Cognitive Routing session presets could not be reset: ${routingResult.reason ?? routingResult.status}.`,
1566
+ "warning",
1567
+ );
1568
+ return { changed: false, reloadRequired: false };
1569
+ }
1570
+ try {
1571
+ const result = await resetSessionOverrides(ctx, pi);
1572
+ return {
1573
+ changed: result.changed || routingResult.status !== "unchanged",
1574
+ reloadRequired: result.reloadRequired,
1575
+ };
1576
+ } catch (error) {
1577
+ ctx.ui.notify(
1578
+ `Routing presets were reset, but core/context session overrides could not be reset: ${
1579
+ error instanceof Error ? error.message : String(error)
1580
+ }`,
1581
+ "warning",
1582
+ );
1583
+ return {
1584
+ changed: routingResult.status !== "unchanged",
1585
+ reloadRequired: false,
1586
+ };
1587
+ }
1588
+ }
1589
+ if (item.configScope === "session" && isCognitiveRoutingProfileItem(item)) {
1590
+ if (!cognitiveRoutingController) {
1591
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1592
+ return { changed: false, reloadRequired: false };
1593
+ }
1594
+ const profile = item.id.split(".").at(-1) as CognitiveRoutingProfileName;
1595
+ const override =
1596
+ value === undefined || value === LOCAL_INHERIT
1597
+ ? null
1598
+ : {
1599
+ provider: (value as CognitiveRoutingProfile).provider,
1600
+ modelId: (value as CognitiveRoutingProfile).model,
1601
+ thinking: (value as CognitiveRoutingProfile).thinking,
1602
+ };
1603
+ const result = await cognitiveRoutingController.setSessionProfileOverride(
1604
+ profile,
1605
+ override,
1606
+ "Session profile settings",
1607
+ );
1608
+ if (result.status === "blocked" || result.reason) {
1609
+ ctx.ui.notify(
1610
+ `Cognitive Routing session preset could not be applied: ${result.reason ?? result.status}.`,
1611
+ "warning",
1612
+ );
1613
+ return { changed: false, reloadRequired: false };
1614
+ }
1615
+ await afterChange(false);
1616
+ return { changed: result.status !== "unchanged", reloadRequired: false };
1387
1617
  }
1388
1618
  if (item.configScope === "session") {
1389
1619
  const keyById = {
@@ -0,0 +1,117 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import assert from "node:assert/strict";
5
+ import test from "node:test";
6
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
7
+ import { RequestHistory } from "../../dist/runtime/request-history.js";
8
+ const state = (text) => ({ role: "custom", customType: "freeflow-runtime-state", content: text, display: false });
9
+ const communication = (text) => ({
10
+ role: "custom",
11
+ customType: "freeflow-routing-communication",
12
+ content: text,
13
+ display: false,
14
+ });
15
+ const user = (text) => ({ role: "user", content: [{ type: "text", text }], timestamp: 1 });
16
+ function fixture(manager = SessionManager.inMemory("/tmp/freeflow-cache")) {
17
+ const pi = { appendEntry: (type, data) => manager.appendCustomEntry(type, data) };
18
+ return { manager, pi, ctx: { sessionManager: manager, ui: { setStatus() {} } }, history: new RequestHistory(pi) };
19
+ }
20
+ test("generated state stays at its original position and changed state appends", async () => {
21
+ const f = fixture(),
22
+ one = await f.history.assemble([state("enabled"), user("one")], "ordinary", f.ctx);
23
+ const two = await f.history.assemble([user("one"), state("disabled"), user("two")], "ordinary", f.ctx);
24
+ assert.deepEqual(two.slice(0, one.length), one);
25
+ assert.equal(two.at(-1).content, "disabled");
26
+ const count = f.manager.getEntries().length;
27
+ assert.deepEqual(await f.history.assemble([state("disabled"), user("one"), user("two")], "ordinary", f.ctx), two);
28
+ assert.equal(f.manager.getEntries().length, count);
29
+ const reload = new RequestHistory(f.pi);
30
+ const three = await reload.assemble([state("disabled"), user("one"), user("two"), user("three")], "ordinary", f.ctx);
31
+ assert.deepEqual(three.slice(0, two.length), two);
32
+ });
33
+ test("view changes never import excluded bodies or stale required communication", async () => {
34
+ const f = fixture();
35
+ await f.history.assemble([user("SECRET"), state("worker"), communication("required report")], "ordinary", f.ctx);
36
+ const coordinator = await f.history.assemble([user("selected"), state("coordinator")], "coordinator", f.ctx);
37
+ assert.ok(!JSON.stringify(coordinator).includes("SECRET"));
38
+ const reduced = await f.history.assemble([user("new visible"), state("worker")], "ordinary", f.ctx);
39
+ assert.ok(!JSON.stringify(reduced).includes("SECRET"));
40
+ assert.ok(!JSON.stringify(reduced).includes("required report"));
41
+ });
42
+ test("context persistence failure returns the current qualified view without old bodies", async () => {
43
+ const f = fixture();
44
+ await f.history.assemble([user("old"), state("first")], "ordinary", f.ctx);
45
+ f.pi.appendEntry = () => {
46
+ throw Error("disk");
47
+ };
48
+ const current = [user("current"), state("second")];
49
+ assert.equal(await f.history.assemble(current, "ordinary", f.ctx), current);
50
+ });
51
+
52
+ test("changing only the view label retains the exact permitted prefix", async () => {
53
+ const f = fixture();
54
+ const first = await f.history.assemble([user("common"), state("ordinary")], "ordinary", f.ctx);
55
+ const next = await f.history.assemble([user("common"), state("coordinator")], "coordinator", f.ctx);
56
+ assert.deepEqual(next.slice(0, first.length), first);
57
+ });
58
+
59
+ test("native labeled forks preserve generated-state positions", async () => {
60
+ const root = await mkdtemp(join(tmpdir(), "freeflow-cache-fork-"));
61
+ try {
62
+ const manager = SessionManager.create(root, root),
63
+ f = fixture(manager);
64
+ const id = manager.appendMessage(user("one"));
65
+ manager.appendLabelChange(id, "named");
66
+ const first = await f.history.assemble([user("one"), state("enabled")], "ordinary", f.ctx);
67
+ manager.appendMessage({
68
+ role: "assistant",
69
+ content: [{ type: "text", text: "done" }],
70
+ timestamp: 2,
71
+ stopReason: "stop",
72
+ });
73
+ const path = manager.createBranchedSession(manager.getLeafId());
74
+ const nextFixture = fixture(SessionManager.open(path));
75
+ const next = await nextFixture.history.assemble(
76
+ [user("one"), user("two"), state("disabled")],
77
+ "ordinary",
78
+ nextFixture.ctx,
79
+ );
80
+ assert.deepEqual(next.slice(0, first.length), first);
81
+ } finally {
82
+ await rm(root, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ test("budget notices see assembled historical overhead and clear without rewriting old notices", async () => {
87
+ const f = fixture();
88
+ let length;
89
+ const first = await f.history.assemble([user("one"), state("enabled")], "ordinary", f.ctx, (messages) => {
90
+ length = messages.length;
91
+ return { role: "custom", customType: "freeflow-routing-budget", content: "Large input", display: false };
92
+ });
93
+ assert.equal(length, 2);
94
+ const next = await f.history.assemble([user("one"), user("two"), state("enabled")], "ordinary", f.ctx, (messages) => {
95
+ length = messages.length;
96
+ });
97
+ assert.equal(length, 4);
98
+ assert.deepEqual(next.slice(0, first.length), first);
99
+ assert.match(next.at(-1).content, /warning cleared/);
100
+ });
101
+
102
+ test("ending a restoration obligation retains already admitted communication until context changes", async () => {
103
+ const f = fixture();
104
+ const first = await f.history.assemble(
105
+ [user("one"), communication("accepted report"), state("active")],
106
+ "ordinary",
107
+ f.ctx,
108
+ );
109
+ const closed = await f.history.assemble([user("one"), user("two"), state("closed")], "ordinary", f.ctx);
110
+ assert.deepEqual(closed.slice(0, first.length), first);
111
+ const again = await f.history.assemble(
112
+ [user("one"), user("two"), communication("accepted report"), state("closed")],
113
+ "ordinary",
114
+ f.ctx,
115
+ );
116
+ assert.equal(again.filter((message) => message.content === "accepted report").length, 1);
117
+ });