@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
@@ -1,32 +1,31 @@
1
1
  import { ROUTING_TOOLS } from "./runtime.js";
2
- import { ROUTING_SCHEMAS, matches, schemaForProfile } from "./schemas.js";
2
+ import { ROUTING_SCHEMAS, matches } from "./schemas.js";
3
3
  export { ROUTING_SCHEMAS } from "./schemas.js";
4
4
  import { renderRoutingCall, renderRoutingResult } from "./render.js";
5
5
  const descriptions = {
6
6
  freeflow_delegate:
7
- "Save a supported assignment contract and request Executor execution. Explicit replace supersedes a quiescent outstanding assignment in the same unit.",
7
+ "Save a supported assignment contract and request enabled worker execution. Explicit replace supersedes a quiescent outstanding assignment in the same unit.",
8
8
  freeflow_return:
9
- "Save or revise the actual assignment report, or retry its saved handoff without resubmitting text. Returning does not accept or close the unit.",
9
+ "Save or revise the actual assignment report, save an attached recovery supplement, or retry the current saved return without resubmitting text. Returning does not accept or close the unit.",
10
10
  freeflow_unit:
11
- "Inspect current responsibility, paginated work history, or exact saved contract/report details. Coordinator can also restore a suspended assessment or close a unit without clearing its history.",
11
+ "Inspect current responsibility, paginated work history, or exact saved communication. Coordinator can restore an assessment, request or cancel attached evidence recovery, or close a unit without clearing history.",
12
12
  freeflow_project:
13
- "Inspect current selection and scoped candidates, then add or remove completed exposed Executor sources. #text refs select exact visible assistant text; plain refs retain whole-native meaning. Eligibility and target readiness are distinct.",
13
+ "Inspect current selection and scoped candidates when needed, then add or remove exposed worker task-evidence refs. Strict shapes: inspect uses operation plus optional scope/cursor; add uses operation plus eligible refs only; remove uses operation plus currently selected or unresolved refs plus required reason. Reason is remove-only. #text refs select exact visible assistant text; plain refs retain whole-native meaning.",
14
14
  };
15
15
  const guidance = {
16
16
  freeflow_delegate: [
17
- "freeflow_delegate is Coordinator-only under Automatic control. Put the contract in the input. Use replace only for explicit same-unit replacement; do not supply IDs.",
17
+ "freeflow_delegate is Coordinator-only under Automatic control. Put the contract in the input. Choose worker when both Helper and Executor are enabled; the sole worker is inferred otherwise. Use replace only for explicit same-unit replacement; do not supply IDs.",
18
18
  ],
19
19
  freeflow_return: [
20
- "freeflow_return submit saves/revises the actual report; retry completes the saved return without text. Stop ordinary task work after reportSaved. Keep the handoff last; do not batch it with new task tools.",
20
+ "freeflow_return submit saves/revises the assignment report; supplement saves separate recovery communication; retry completes the current unchanged saved return without text. Stop ordinary task or recovery-read work after its communication is saved. Keep the handoff last; do not batch it with new task tools.",
21
21
  ],
22
22
  freeflow_unit: [
23
- "freeflow_unit inspect defaults to current state; view history returns work refs and view detail with ref reads saved contracts/reports. Coordinator assess restores a suspended evidence obligation; close changes work status without clearing communication or completing a Working Record task.",
23
+ "freeflow_unit inspect defaults to current state; view history returns work refs and detail reads saved communication. Coordinator recover requests bounded evidence work against the current returned assessment; cancel-recovery ends only that recovery; assess restores a suspended evidence obligation; close changes work status without clearing communication or completing a Working Record task.",
24
24
  ],
25
25
  freeflow_project: [
26
- "freeflow_project inspect shows selection and scoped candidates. Use history scope for earlier exposed evidence. #text refs select exact visible assistant text; plain refs retain whole-native meaning. Select actual result bodies for execution claims, not merely calls. Correct or explicitly withdraw unresolved items with a limitation. Known previously exposed bodies resolve automatically; target-representation gaps are not successful delivery.",
26
+ "Use exactly one shape: inspect {operation, scope?, cursor?}; add {operation, refs} with no reason; remove {operation, refs, reason}. Reason is required only for remove and invalid for add/inspect. Add exact eligible visible refs directly when identity and eligibility are clear; inspect when identity, eligibility, representation or selection state is unclear. Never add refs marked not offered for new evidence selection. For remove, use currently selected or unresolved refs; a non-selectable ref may be named only to clear its unresolved request and requires a reason. Use an offered #text ref only when visible assistant text is the intended evidence. Plain refs retain whole-native meaning. Select actual result bodies for execution claims, not merely calls. Correct or explicitly withdraw unresolved items with a limitation. Known previously exposed bodies resolve automatically; target-representation gaps are not successful delivery.",
27
27
  ],
28
28
  };
29
- const unitDefinitions = new WeakMap();
30
29
  export function registerRoutingTools(pi, runtime) {
31
30
  for (const name of ROUTING_TOOLS) {
32
31
  const definition = {
@@ -45,47 +44,17 @@ export function registerRoutingTools(pi, runtime) {
45
44
  },
46
45
  };
47
46
  pi.registerTool(definition);
48
- if (name === "freeflow_unit") unitDefinitions.set(pi, { definition });
49
47
  }
50
48
  }
51
- export function applyRoutingToolVisibility(pi, runtime, available) {
49
+ export function applyRoutingToolVisibility(pi, _runtime, _available = true) {
52
50
  if (!pi.getActiveTools || !pi.setActiveTools) return;
53
- const current = new Set(pi.getActiveTools());
54
- const state = runtime.state();
55
- const unit = unitDefinitions.get(pi);
56
- const coordinator = state.effective && state.controlMode === "automatic" && state.activeProfile === "coordinator";
57
- const signature = coordinator ? "coordinator" : "inspection";
58
- if (unit && unit.signature !== signature) {
59
- pi.registerTool({
60
- ...unit.definition,
61
- description: coordinator
62
- ? unit.definition.description
63
- : "Inspect current routing responsibility, work history, or exact saved contract/report detail. This profile cannot assess or close units.",
64
- promptGuidelines: coordinator
65
- ? unit.definition.promptGuidelines
66
- : [
67
- "freeflow_unit inspect reads current state, view history lists work refs, and view detail with ref reads historical communication. Historical content is not current permission.",
68
- ],
69
- parameters: schemaForProfile("freeflow_unit", coordinator),
70
- });
71
- unit.signature = signature;
72
- }
73
- for (const name of ROUTING_TOOLS) {
74
- const enabled =
75
- available &&
76
- (name === "freeflow_unit" ||
77
- (state.effective &&
78
- state.controlMode === "automatic" &&
79
- (name === "freeflow_delegate"
80
- ? state.activeProfile === "coordinator"
81
- : state.activeProfile === "executor"))) &&
82
- (name !== "freeflow_project" || runtime.projectionEnabled);
83
- if (enabled) current.add(name);
84
- else current.delete(name);
85
- }
86
- // Retired experimental tools must never remain available beside the replacement.
87
- current.delete("freeflow_switch_profile");
88
- current.delete("freeflow_cognitive_routing_history");
89
- const next = [...current];
90
- if (JSON.stringify(next) !== JSON.stringify(pi.getActiveTools())) pi.setActiveTools(next);
51
+ const current = pi.getActiveTools();
52
+ const ordinary = current.filter(
53
+ (name) =>
54
+ !ROUTING_TOOLS.includes(name) &&
55
+ name !== "freeflow_switch_profile" &&
56
+ name !== "freeflow_cognitive_routing_history",
57
+ );
58
+ const next = [...ordinary, ...ROUTING_TOOLS];
59
+ if (JSON.stringify(next) !== JSON.stringify(current)) pi.setActiveTools(next);
91
60
  }
@@ -1,7 +1,11 @@
1
1
  export const ROUTING_ENTRY = "freeflow-routing-v2";
2
2
  export const ROUTING_MESSAGE = "freeflow-routing-v2-state";
3
- export const PROFILES = ["coordinator", "executor"];
3
+ export const WORKER_PROFILES = ["helper", "executor"];
4
+ export const PROFILES = ["coordinator", ...WORKER_PROFILES];
5
+ export const DELEGATION_MODES = ["executor", "helper", "both"];
4
6
  export const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
7
+ export const isWorkerProfile = (value) => WORKER_PROFILES.includes(value);
8
+ export const workersForDelegation = (mode) => (mode === "both" ? WORKER_PROFILES : [mode]);
5
9
  export class RoutingError extends Error {
6
10
  code;
7
11
  problems;
@@ -75,10 +75,6 @@ export class ContextVirtualizationRuntime {
75
75
  return !projection || projection.mode === "full";
76
76
  }
77
77
  async project(messages, enabled) {
78
- if (!enabled) {
79
- this.freeflowContext.clearRequest();
80
- return { messages, changed: false, available: true };
81
- }
82
78
  if (!(await this.recover(this.ctx))) {
83
79
  return { messages, changed: false, available: false };
84
80
  }
@@ -92,19 +88,45 @@ export class ContextVirtualizationRuntime {
92
88
  const ref = contextRefForEntry(source.source.entryId);
93
89
  refs.set(ref, source);
94
90
  const state = this.projectionState.get(source.source.entryId);
95
- const projection = sourceProjection(state);
91
+ const projection = enabled ? sourceProjection(state) : { mode: "full" };
96
92
  const nextMessage = projectToolResultMessage(message, source.source, projection);
97
93
  if (!safeJsonEqual(nextMessage.content, message.content)) changed = true;
98
94
  return nextMessage;
99
95
  });
100
- const request = this.freeflowContext.recordRequest(refs);
96
+ const request = enabled ? this.freeflowContext.recordRequest(refs) : undefined;
97
+ if (!enabled) this.freeflowContext.clearRequest();
101
98
  return {
102
99
  messages: changed ? projected : messages,
103
100
  changed,
104
101
  available: true,
105
- generation: request.generation,
102
+ generation: request?.generation,
106
103
  };
107
104
  }
105
+ decorate(messages) {
106
+ try {
107
+ const sources = this.freeflowContext.resolver.toolResultSources();
108
+ return messages.map((message) => {
109
+ if (message?.role !== "toolResult") return message;
110
+ const source = sources.get(message.toolCallId);
111
+ if (!source) return message;
112
+ const marker = `[context-ref: ${contextRefForEntry(source.source.entryId)}]`;
113
+ if (
114
+ (Array.isArray(message.content) ? message.content : []).some(
115
+ (part) =>
116
+ part.type === "text" &&
117
+ typeof part.text === "string" &&
118
+ (part.text === marker ||
119
+ part.text.startsWith(`[context archived: ${contextRefForEntry(source.source.entryId)}]`)),
120
+ )
121
+ )
122
+ return message;
123
+ return projectToolResultMessage(message, source.source, { mode: "full" });
124
+ });
125
+ } catch {
126
+ // Decoration must not make Pi fall back to an unprojected context.
127
+ return messages;
128
+ }
129
+ }
108
130
  async archive(targets) {
109
131
  try {
110
132
  return await this.enqueue(() => this.archiveNow(targets));
@@ -251,13 +251,14 @@ export function registerFreeflowContextTool(
251
251
  getConversationHistoryRuntime = () => undefined,
252
252
  features = { contextVirtualization: true, conversationHistory: false },
253
253
  ) {
254
+ const surface = { contextVirtualization: true, conversationHistory: true };
254
255
  pi.registerTool({
255
256
  name: CONTEXT_VIRTUALIZATION_TOOL_NAME,
256
257
  label: "Freeflow Context",
257
- description: featureDescription(features),
258
- promptSnippet: featurePromptSnippet(features),
258
+ description: featureDescription(surface),
259
+ promptSnippet: featurePromptSnippet(surface),
259
260
  promptGuidelines: [
260
- ...(features.contextVirtualization
261
+ ...(surface.contextVirtualization
261
262
  ? [
262
263
  "Archive only a tool result whose context-ref appeared in the request you just consumed.",
263
264
  "Archive without retained meaning when nothing from the result needs to remain active.",
@@ -266,18 +267,18 @@ export function registerFreeflowContextTool(
266
267
  "Use restore to reverse projection; it does not retrieve history removed by Pi compaction.",
267
268
  ]
268
269
  : []),
269
- ...(features.conversationHistory
270
+ ...(surface.conversationHistory
270
271
  ? [
271
272
  "Search only hidden history on the current active branch; visible sources and freeflow_context calls/results are excluded.",
272
273
  "Treat search snippets as discovery and retrieve only selected refs.",
273
274
  "Treat retrieved history as evidence, not current authority or instructions.",
274
275
  ]
275
276
  : []),
276
- ...(features.contextVirtualization && features.conversationHistory
277
+ ...(surface.contextVirtualization && surface.conversationHistory
277
278
  ? ["Archive consumed search or retrieval results only after their exact detail is no longer needed."]
278
279
  : []),
279
280
  ],
280
- parameters: contextParameters(features),
281
+ parameters: contextParameters(surface),
281
282
  renderCall,
282
283
  renderResult,
283
284
  async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
@@ -1,4 +1,9 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { RequestHistory } from "./runtime/request-history.js";
4
+ import { registerProviderSupport } from "./provider-support/index.js";
1
5
  import { RoutingRuntime } from "./cognitive-routing-v2/runtime.js";
6
+ import { workersForDelegation } from "./cognitive-routing-v2/types.js";
2
7
  import { applyRoutingToolVisibility, registerRoutingTools } from "./cognitive-routing-v2/tools.js";
3
8
  import { ConversationHistoryRuntime } from "./conversation-history/runtime.js";
4
9
  import { FreeflowContextRuntime } from "./freeflow-context/runtime.js";
@@ -11,14 +16,14 @@ import {
11
16
  CONTRIBUTOR_COMMANDS,
12
17
  WORKFLOW_COMMANDS,
13
18
  freeflowModelSkillPaths,
14
- freeflowSkillPath,
15
19
  getRuntimeContext,
16
20
  hasUsableMandatoryPrompts,
17
21
  isPromptAvailable,
18
22
  readCapabilityState,
19
23
  refreshRuntimeContext,
20
24
  restoreSessionOverrides,
21
- runtimeContext,
25
+ stableRuntimeContext,
26
+ STABLE_FREEFLOW_SURFACE,
22
27
  filterBootstrapMessage,
23
28
  setFreeflowStatus,
24
29
  skillPrompt,
@@ -52,7 +57,8 @@ function freeflowCompletions(prefix, routingAvailable) {
52
57
  : query.startsWith("profile ") && routingAvailable
53
58
  ? [
54
59
  ["profile coordinator", "coordinator", "Hold Coordinator manually"],
55
- ["profile executor", "executor", "Hold Executor manually"],
60
+ ["profile helper", "helper", "Hold Helper manually when enabled"],
61
+ ["profile executor", "executor", "Hold Executor manually when enabled"],
56
62
  ["profile auto", "auto", "Return to automatic Coordinator reconciliation"],
57
63
  ["profile history", "history", "Read routing observations"],
58
64
  ]
@@ -81,8 +87,11 @@ function freeflowCompletions(prefix, routingAvailable) {
81
87
  .map(([value, label, description]) => ({ value, label, description }));
82
88
  }
83
89
  export default function freeflow(pi) {
90
+ registerProviderSupport(pi);
84
91
  const api = pi;
85
- const routing = new RoutingRuntime(api);
92
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
93
+ const routing = new RoutingRuntime(api, [packageRoot]);
94
+ const requestHistory = new RequestHistory(api);
86
95
  let capability;
87
96
  let prompts;
88
97
  let context;
@@ -99,7 +108,7 @@ export default function freeflow(pi) {
99
108
  async function loadSurface(ctx) {
100
109
  const generation = surfaceGeneration;
101
110
  const next = await readCapabilityState(ctx.cwd, ctx, pi.host);
102
- const loaded = await getRuntimeContext(next);
111
+ const loaded = await getRuntimeContext(STABLE_FREEFLOW_SURFACE);
103
112
  if (generation !== surfaceGeneration) throw new Error("Discarded surface preparation for a replaced session.");
104
113
  if (!hasUsableMandatoryPrompts(loaded)) {
105
114
  for (const key of ["cognitiveRouting", "contextVirtualization", "conversationHistory"])
@@ -147,9 +156,7 @@ export default function freeflow(pi) {
147
156
  );
148
157
  if (api.getActiveTools && api.setActiveTools) {
149
158
  const current = new Set(api.getActiveTools());
150
- if (capability?.contextVirtualization?.effective || capability?.conversationHistory?.effective)
151
- current.add(CONTEXT_VIRTUALIZATION_TOOL_NAME);
152
- else current.delete(CONTEXT_VIRTUALIZATION_TOOL_NAME);
159
+ current.add(CONTEXT_VIRTUALIZATION_TOOL_NAME);
153
160
  const next = [...current];
154
161
  if (JSON.stringify(next) !== JSON.stringify(api.getActiveTools())) api.setActiveTools(next);
155
162
  }
@@ -174,18 +181,18 @@ export default function freeflow(pi) {
174
181
  );
175
182
  pi.on("resources_discover", async (event, ctx) => {
176
183
  const state = capability ?? (await loadSurface(ctx ?? { cwd: event?.cwd ?? process.cwd() }));
177
- if (!state.configured) return { skillPaths: [freeflowSkillPath("setup-freeflow")] };
178
- return { skillPaths: state.enabled && hasUsableMandatoryPrompts(prompts) ? freeflowModelSkillPaths(state) : [] };
184
+ return { skillPaths: freeflowModelSkillPaths(STABLE_FREEFLOW_SURFACE) };
179
185
  });
180
186
  pi.on("session_start", async (event, ctx) => {
181
187
  const generation = ++surfaceGeneration;
188
+ requestHistory.reset();
182
189
  routing.unbind();
183
190
  capability = undefined;
184
191
  prompts = undefined;
185
192
  refreshState = true;
186
193
  restoreSessionOverrides(ctx);
187
194
  const initial = await readCapabilityState(ctx.cwd, ctx, pi.host);
188
- await refreshRuntimeContext(initial);
195
+ await refreshRuntimeContext(STABLE_FREEFLOW_SURFACE);
189
196
  if (generation !== surfaceGeneration) return;
190
197
  await loadSurface(ctx);
191
198
  context = new FreeflowContextRuntime(ctx);
@@ -202,6 +209,7 @@ export default function freeflow(pi) {
202
209
  });
203
210
  pi.on("session_shutdown", async () => {
204
211
  surfaceGeneration++;
212
+ requestHistory.reset();
205
213
  routing.unbind();
206
214
  context = undefined;
207
215
  virtualization = undefined;
@@ -214,7 +222,7 @@ export default function freeflow(pi) {
214
222
  await update(ctx);
215
223
  await routing.beforeRun(ctx);
216
224
  status(ctx);
217
- const text = runtimeContext(prompts, capability);
225
+ const text = stableRuntimeContext(prompts);
218
226
  return { systemPrompt: text ? `${event.systemPrompt}\n\n${text}` : event.systemPrompt };
219
227
  });
220
228
  pi.on("message_end", (event) => routing.messageEnd(event.message));
@@ -260,9 +268,19 @@ export default function freeflow(pi) {
260
268
  projectionFailure: routing.state().projectionFailure,
261
269
  });
262
270
  refreshState = false;
263
- return { messages: await routing.context(ctx, messages) };
271
+ const routed = await routing.context(ctx, messages);
272
+ const projected = virtualization?.decorate(routed) ?? routed;
273
+ const state = routing.state();
274
+ if (projected.some((message) => message.details?.routingRequestBlocked === true)) return { messages: projected };
275
+ const view = routing.projectionEnabled && state.activeProfile === "coordinator" ? "coordinator" : "ordinary";
276
+ return {
277
+ messages: await requestHistory.assemble(projected, view, ctx, (assembled) =>
278
+ routing.budgetNotice(assembled, ctx),
279
+ ),
280
+ };
264
281
  });
265
282
  const restore = async (ctx, navigation = true) => {
283
+ requestHistory.reset();
266
284
  restoreSessionOverrides(ctx);
267
285
  refreshState = true;
268
286
  if (virtualization) {
@@ -286,13 +304,16 @@ export default function freeflow(pi) {
286
304
  typeof api.setThinkingLevel === "function"
287
305
  ) {
288
306
  pi.registerShortcut("ctrl+shift+r", {
289
- description: "Cycle the Coordinator/Executor manual hold",
307
+ description: "Cycle enabled Cognitive Routing manual holds",
290
308
  handler: async (ctx) => {
291
309
  if (!ctx.isIdle()) {
292
310
  ctx.ui.notify("Wait for Pi to become idle before changing control.", "warning");
293
311
  return;
294
312
  }
295
- const target = routing.state().activeProfile === "coordinator" ? "executor" : "coordinator";
313
+ const state = routing.state();
314
+ const profiles = ["coordinator", ...workersForDelegation(state.delegation)];
315
+ const current = profiles.indexOf(state.activeProfile ?? "coordinator");
316
+ const target = profiles[(current + 1) % profiles.length];
296
317
  ctx.ui.notify(JSON.stringify(await routing.setManualProfile(target)), "info");
297
318
  await update(ctx);
298
319
  },
@@ -0,0 +1,110 @@
1
+ import { assemble, EFFORTS } from "./history.js";
2
+ import { SessionState } from "./session-state.js";
3
+ export function requestKey(payload, model) {
4
+ if (
5
+ !payload ||
6
+ payload.model !== "gpt-6-astra" ||
7
+ model?.id !== payload.model ||
8
+ payload.store !== false ||
9
+ !Array.isArray(payload.input) ||
10
+ !EFFORTS.includes(payload.reasoning?.effort) ||
11
+ payload.previous_response_id ||
12
+ payload.conversation ||
13
+ payload.context_management ||
14
+ (payload.truncation && payload.truncation !== "disabled") ||
15
+ (payload.reasoning.mode && payload.reasoning.mode !== "standard") ||
16
+ payload.agents ||
17
+ payload.input.some((x) => x?.type === "configuration_update" || x?.type === "compaction_trigger")
18
+ )
19
+ return;
20
+ const codex = model.provider === "openai-codex" && model.api === "openai-codex-responses";
21
+ const api = model.provider === "openai" && model.api === "openai-responses";
22
+ if (!codex && !api) return;
23
+ if (model.baseUrl !== undefined && typeof model.baseUrl !== "string") return;
24
+ const base = (model.baseUrl ?? (codex ? "https://chatgpt.com/backend-api" : "https://api.openai.com/v1")).replace(
25
+ /\/+$/,
26
+ "",
27
+ );
28
+ const allowed = codex
29
+ ? [
30
+ "https://chatgpt.com/backend-api",
31
+ "https://chatgpt.com/backend-api/codex",
32
+ "https://chatgpt.com/backend-api/codex/responses",
33
+ ]
34
+ : ["https://api.openai.com/v1"];
35
+ if (!allowed.includes(base)) return;
36
+ return `${model.provider}/${model.api}/${model.id}`;
37
+ }
38
+ export class AstraAdapter {
39
+ pi;
40
+ session;
41
+ generation = 0;
42
+ compacting = false;
43
+ queue = Promise.resolve();
44
+ statusSignature;
45
+ constructor(pi) {
46
+ this.pi = pi;
47
+ }
48
+ reset(ctx) {
49
+ this.generation++;
50
+ this.session = undefined;
51
+ this.compacting = false;
52
+ this.status(ctx, undefined);
53
+ }
54
+ setCompacting(value) {
55
+ this.compacting = value;
56
+ }
57
+ status(ctx, label) {
58
+ if (this.statusSignature === label) return;
59
+ this.statusSignature = label;
60
+ // Diagnostics must never turn a completed payload transformation into an exception.
61
+ try {
62
+ ctx.ui?.setStatus?.("freeflow-astra-effort", label);
63
+ } catch {}
64
+ }
65
+ async adapt(payload, ctx) {
66
+ const generation = this.generation;
67
+ const operation = this.queue.then(async () => {
68
+ const key = requestKey(payload, ctx.model);
69
+ if (generation !== this.generation || this.compacting || !key) {
70
+ this.status(ctx, undefined);
71
+ return payload;
72
+ }
73
+ try {
74
+ const reader = ctx.sessionManager;
75
+ if (
76
+ typeof this.pi.appendEntry !== "function" ||
77
+ !reader?.getLeafId ||
78
+ !reader.getSessionId ||
79
+ !reader.getSessionFile
80
+ )
81
+ throw new Error("Native session persistence unavailable");
82
+ const id = reader.getSessionId(),
83
+ file = reader.getSessionFile();
84
+ if (!this.session || this.session.id !== id || this.session.file !== file)
85
+ this.session = { id, file, store: new SessionState(this.pi, reader) };
86
+ const store = this.session.store,
87
+ basis = reader.getLeafId();
88
+ const records = await store.records();
89
+ if (
90
+ generation !== this.generation ||
91
+ reader.getLeafId() !== basis ||
92
+ this.session?.store !== store ||
93
+ key !== requestKey(payload, ctx.model)
94
+ )
95
+ throw new Error("Astra request changed during preparation");
96
+ const result = assemble(payload, key, store.generation(), basis, records);
97
+ if (result.record) store.append(result.record);
98
+ this.status(ctx, `Astra ${result.effective} · cache baseline ${result.baseline}`);
99
+ return result.payload;
100
+ } catch {
101
+ // Pi swallows hook exceptions. Return the untouched full-history request,
102
+ // including its requested effort, instead of a partially pinned request.
103
+ this.status(ctx, "Astra cache adaptation unavailable · native effort");
104
+ return payload;
105
+ }
106
+ });
107
+ this.queue = operation.catch(() => {});
108
+ return operation;
109
+ }
110
+ }
@@ -0,0 +1,125 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ export const ENTRY_TYPE = "freeflow-astra-effort-v1";
3
+ export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
4
+ export const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
5
+ const digest = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
6
+ const effort = (x) => EFFORTS.includes(x);
7
+ const identity = (x) => typeof x === "string" && x.length > 0 && x.length <= 1024;
8
+ export function parseAttempt(value) {
9
+ if (
10
+ !value ||
11
+ value.version !== 1 ||
12
+ !identity(value.id) ||
13
+ !(value.basis === null || identity(value.basis)) ||
14
+ !identity(value.key) ||
15
+ !identity(value.generation) ||
16
+ !digest(value.envelope) ||
17
+ !digest(value.inputHash) ||
18
+ !effort(value.baseline) ||
19
+ !effort(value.effective) ||
20
+ !(value.parent === null || identity(value.parent)) ||
21
+ !Number.isSafeInteger(value.length) ||
22
+ value.length < 0 ||
23
+ (value.update !== undefined &&
24
+ (!value.update ||
25
+ !Number.isSafeInteger(value.update.at) ||
26
+ value.update.at < 0 ||
27
+ value.update.at > value.length ||
28
+ !digest(value.update.prefixHash) ||
29
+ !effort(value.update.effort))) ||
30
+ Object.keys(value).some(
31
+ (k) =>
32
+ ![
33
+ "version",
34
+ "id",
35
+ "basis",
36
+ "key",
37
+ "generation",
38
+ "envelope",
39
+ "baseline",
40
+ "effective",
41
+ "parent",
42
+ "length",
43
+ "inputHash",
44
+ "update",
45
+ ].includes(k),
46
+ )
47
+ )
48
+ throw new Error("Invalid Astra effort history");
49
+ return value;
50
+ }
51
+ // Hash complete serialized items, including opaque reasoning; never persist their bodies.
52
+ export function prefixHashes(input) {
53
+ const result = [hash([])];
54
+ for (const item of input) result.push(hash([result.at(-1), item]));
55
+ return result;
56
+ }
57
+ export function envelopeHash(payload) {
58
+ const { input: _input, prompt_cache_key: _key, max_output_tokens: _max, stream: _stream, ...rest } = payload;
59
+ const { effort: _effort, ...reasoning } = rest.reasoning;
60
+ return hash({ ...rest, reasoning });
61
+ }
62
+ export function assemble(payload, key, generation, basis, records) {
63
+ const hashes = prefixHashes(payload.input),
64
+ envelope = envelopeHash(payload);
65
+ const applicable = records.filter((r) => r.key === key && r.generation === generation);
66
+ const compatible = applicable.filter((r) => r.envelope === envelope && hashes[r.length] === r.inputHash);
67
+ const prior = compatible.reduce((best, r) => (!best || r.length >= best.length ? r : best), undefined);
68
+ const baseline = prior?.baseline ?? applicable[0]?.baseline ?? payload.reasoning.effort;
69
+ const desired = payload.reasoning.effort;
70
+ const byId = new Map(records.map((r) => [r.id, r]));
71
+ const updates = [];
72
+ const seen = new Set();
73
+ let cursor = prior;
74
+ while (cursor) {
75
+ if (seen.has(cursor.id)) throw new Error("Cyclic Astra history");
76
+ seen.add(cursor.id);
77
+ if (cursor.update) {
78
+ if (hashes[cursor.update.at] !== cursor.update.prefixHash) throw new Error("Astra anchor changed");
79
+ updates.unshift(cursor.update);
80
+ }
81
+ if (cursor.parent === null) break;
82
+ const parent = byId.get(cursor.parent);
83
+ if (!parent || parent.key !== key || parent.generation !== generation || parent.baseline !== baseline)
84
+ throw new Error("Astra history parent unavailable");
85
+ cursor = parent;
86
+ }
87
+ let update;
88
+ if (desired !== (prior?.effective ?? baseline)) {
89
+ // Only insert before a new user item. Within a tool loop, append after all completed exchanges.
90
+ const last = payload.input.length - 1;
91
+ const at = payload.input[last]?.role === "user" && last >= (prior?.length ?? 0) ? last : payload.input.length;
92
+ update = { at, prefixHash: hashes[at], effort: desired };
93
+ if (updates.at(-1)?.at === at) throw new Error("Adjacent Astra effort updates");
94
+ updates.push(update);
95
+ }
96
+ if (updates.some((u, i) => i > 0 && u.at <= updates[i - 1].at)) throw new Error("Invalid Astra update order");
97
+ const output = structuredClone(payload);
98
+ output.input = [];
99
+ let nextUpdate = 0;
100
+ for (let i = 0; i <= payload.input.length; i++) {
101
+ if (updates[nextUpdate]?.at === i) {
102
+ output.input.push({ type: "configuration_update", reasoning: { effort: updates[nextUpdate++].effort } });
103
+ }
104
+ if (i < payload.input.length) output.input.push(structuredClone(payload.input[i]));
105
+ }
106
+ output.reasoning.effort = baseline;
107
+ const identical = prior?.length === payload.input.length && prior.effective === desired;
108
+ const record = identical
109
+ ? undefined
110
+ : {
111
+ version: 1,
112
+ id: randomUUID(),
113
+ basis,
114
+ key,
115
+ generation,
116
+ envelope,
117
+ baseline,
118
+ effective: desired,
119
+ parent: prior?.id ?? null,
120
+ length: payload.input.length,
121
+ inputHash: hashes.at(-1),
122
+ ...(update ? { update } : {}),
123
+ };
124
+ return { payload: output, record, baseline, effective: desired, rebased: !prior && applicable.length > 0 };
125
+ }
@@ -0,0 +1,11 @@
1
+ import { AstraAdapter } from "./adapter.js";
2
+ export function registerAstraSupport(pi) {
3
+ const adapter = new AstraAdapter(pi);
4
+ pi.on("session_start", (_event, ctx) => adapter.reset(ctx));
5
+ pi.on("session_shutdown", (_event, ctx) => adapter.reset(ctx));
6
+ pi.on("session_tree", (_event, ctx) => adapter.reset(ctx));
7
+ pi.on("session_before_compact", () => adapter.setCompacting(true));
8
+ pi.on("session_compact", (_event, ctx) => adapter.reset(ctx));
9
+ pi.on("session_compact_failed", () => adapter.setCompacting(false));
10
+ pi.on("before_provider_request", (event, ctx) => adapter.adapt(event.payload, ctx));
11
+ }