@hassangameryt/freeflow 0.7.3 → 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.
Files changed (276) 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/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Keep a remembered cache entry reachable. A provider only reads an entry that lies within its
3
+ * lookback window of some breakpoint, so when a request extends an earlier one by more than that
4
+ * window (for example after another model's turns were inserted), the earlier entry is lost even
5
+ * though the prefix is unchanged. Placing a breakpoint on the earlier request's final entry lets
6
+ * the request read it, and only the new tail is written.
7
+ */
8
+ export function planAnchor(prior, current, limits) {
9
+ let entry;
10
+ for (const layout of prior) {
11
+ const end = layout.breakpoints.at(-1);
12
+ if (end === undefined || end >= current.chain.length || layout.chain[end] !== current.chain[end]) continue;
13
+ if (entry === undefined || end > entry) entry = end;
14
+ }
15
+ if (entry === undefined || current.breakpoints.includes(entry)) return;
16
+ const reach = current.breakpoints.find((position) => position > entry);
17
+ if (reach === undefined || reach - entry <= limits.lookback - limits.margin) return;
18
+ if (current.breakpoints.length < limits.maxBreakpoints) return { add: entry };
19
+ const remove = current.yieldable.find((position) => position < entry && current.breakpoints.includes(position));
20
+ return remove === undefined ? undefined : { add: entry, remove };
21
+ }
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Anthropic Messages: four explicit breakpoints, each reading entries up to 20 positions back. */
3
+ export const ANTHROPIC_LIMITS = { maxBreakpoints: 4, lookback: 20, margin: 5 };
4
+ // Consecutive tool_use blocks, and consecutive tool_result blocks, form one lookback position.
5
+ const RUN_TYPES = new Set(["tool_use", "tool_result"]);
6
+ // Thinking blocks cannot carry cache_control.
7
+ const UNMARKABLE = new Set(["thinking", "redacted_thinking"]);
8
+ const withoutMarkers = (value) => JSON.stringify(value, (key, v) => (key === "cache_control" ? undefined : v));
9
+ export function anthropicLayout(payload) {
10
+ if (!payload || !Array.isArray(payload.messages)) return;
11
+ const chain = [],
12
+ handles = [],
13
+ breakpoints = [];
14
+ let hash = "",
15
+ pending = "";
16
+ const push = (fingerprint, handle, marked) => {
17
+ hash = createHash("sha256").update(hash).update(pending).update(fingerprint).digest("hex");
18
+ pending = "";
19
+ if (marked) breakpoints.push(chain.length);
20
+ chain.push(hash);
21
+ handles.push(handle);
22
+ };
23
+ if (Array.isArray(payload.tools) && payload.tools.length)
24
+ push(
25
+ `tools:${withoutMarkers(payload.tools)}`,
26
+ { kind: "tools" },
27
+ payload.tools.some((t) => t?.cache_control),
28
+ );
29
+ const system = typeof payload.system === "string" ? [{ type: "text", text: payload.system }] : (payload.system ?? []);
30
+ system.forEach((block, index) =>
31
+ push(
32
+ `system:${withoutMarkers(block)}`,
33
+ typeof payload.system === "string" ? undefined : { kind: "system", index },
34
+ !!block?.cache_control,
35
+ ),
36
+ );
37
+ payload.messages.forEach((message, m) => {
38
+ const start = `message:${message.role}:`;
39
+ if (!Array.isArray(message.content)) {
40
+ // Unmarkable without reshaping the message, but still part of the prefix.
41
+ push(`${start}${withoutMarkers(message.content)}`, undefined, false);
42
+ return;
43
+ }
44
+ if (!message.content.length) {
45
+ // Content-less messages (such as per-message effort) change the prefix without adding a position.
46
+ pending += `${start}${withoutMarkers(message)}`;
47
+ return;
48
+ }
49
+ // Mid-conversation role:"system" updates carry markers too; Pi puts the final one there when an update ends the request.
50
+ const markable = message.role === "user" || message.role === "assistant" || message.role === "system";
51
+ let b = 0;
52
+ while (b < message.content.length) {
53
+ const type = message.content[b]?.type;
54
+ let end = b;
55
+ if (RUN_TYPES.has(type)) while (message.content[end + 1]?.type === type) end++;
56
+ const run = message.content.slice(b, end + 1);
57
+ push(
58
+ `${b === 0 ? start : ""}${withoutMarkers(run)}`,
59
+ markable && !UNMARKABLE.has(message.content[end]?.type) ? { kind: "block", message: m, block: end } : undefined,
60
+ run.some((block) => block?.cache_control),
61
+ );
62
+ b = end + 1;
63
+ }
64
+ });
65
+ if (pending) push("", undefined, false);
66
+ const systemMarks = breakpoints.filter((p) => handles[p]?.kind === "system");
67
+ const yieldable = [
68
+ ...breakpoints.filter((p) => handles[p]?.kind === "tools"),
69
+ // The last system breakpoint covers every earlier one while the system prompt is unchanged.
70
+ ...systemMarks.slice(0, -1),
71
+ ];
72
+ return { layout: { chain, breakpoints, yieldable }, handles };
73
+ }
74
+ function markerAt(payload, handle) {
75
+ if (handle?.kind === "tools") return payload.tools.findLast((t) => t.cache_control)?.cache_control;
76
+ if (handle?.kind === "system") return payload.system[handle.index].cache_control;
77
+ if (handle?.kind === "block") return payload.messages[handle.message].content[handle.block].cache_control;
78
+ }
79
+ /** Apply a plan without mutating the host payload: only the touched containers are copied. */
80
+ export function applyAnthropicAnchor(payload, { layout, handles }, plan) {
81
+ const add = handles[plan.add];
82
+ // Reuse the final breakpoint's marker so TTL ordering (longer lifetimes first) is preserved.
83
+ const marker = markerAt(payload, handles[layout.breakpoints.at(-1)]);
84
+ if (add?.kind !== "block" || !marker) return payload;
85
+ const next = { ...payload, messages: [...payload.messages] };
86
+ const message = { ...next.messages[add.message], content: [...next.messages[add.message].content] };
87
+ message.content[add.block] = { ...message.content[add.block], cache_control: marker };
88
+ next.messages[add.message] = message;
89
+ const remove = plan.remove === undefined ? undefined : handles[plan.remove];
90
+ if (remove?.kind === "tools")
91
+ next.tools = payload.tools.map((tool) => {
92
+ if (!tool?.cache_control) return tool;
93
+ const { cache_control: _marker, ...rest } = tool;
94
+ return rest;
95
+ });
96
+ else if (remove?.kind === "system") {
97
+ next.system = [...payload.system];
98
+ const { cache_control: _marker, ...rest } = next.system[remove.index];
99
+ next.system[remove.index] = rest;
100
+ }
101
+ return next;
102
+ }
@@ -0,0 +1,73 @@
1
+ const WINDOW = 8;
2
+ const MISSES = 4;
3
+ const GAP_MS = 5 * 60_000;
4
+ const MIN_PROMPT = 8_000;
5
+ const HIT_SHARE = 0.5;
6
+ const tokens = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1e3)}k`);
7
+ /**
8
+ * Detects prompt-cache regressions: back-to-back requests to the same model and effort, with nothing in between
9
+ * that legitimately invalidates the cache, should reuse the previous prompt. Provider-side misses are uncommon
10
+ * (about 2-4% of Codex requests in September 2026 sessions, with or without Freeflow), so repeated misses in that
11
+ * situation mean something is rewriting the start of the prompt.
12
+ * A routing profile that hands off and later resumes on the same model should likewise reuse its own
13
+ * previous prompt while that prompt's cache entry lives.
14
+ */
15
+ export class CacheHealth {
16
+ previous;
17
+ lanes = new Map();
18
+ windows = new Map();
19
+ warned = new Map();
20
+ /** Compaction, navigation, or reload legitimately invalidates every cached prompt. */
21
+ noteBreak() {
22
+ this.previous = undefined;
23
+ this.lanes.clear();
24
+ }
25
+ /** A model or effort change starts a new back-to-back run; a routing lane keeps its own prompt. */
26
+ noteSwitch() {
27
+ this.previous = undefined;
28
+ }
29
+ reset() {
30
+ this.previous = undefined;
31
+ this.lanes.clear();
32
+ this.windows.clear();
33
+ this.warned.clear();
34
+ }
35
+ /** Returns a warning the first time a model's recent comparable requests cross the miss threshold. */
36
+ observe(observation) {
37
+ const key = `${observation.provider ?? "unknown"}/${observation.model ?? "unknown"}/${observation.thinking ?? "unknown"}`;
38
+ const prompt = observation.input + observation.cacheRead + (observation.cacheWrite ?? 0);
39
+ const previous = this.previous;
40
+ this.previous = { key, lane: observation.lane, at: observation.at, prompt };
41
+ let basis;
42
+ // Routing profiles see different views of the conversation, so one never reuses another's prompt.
43
+ if (
44
+ previous &&
45
+ previous.key === key &&
46
+ previous.lane === observation.lane &&
47
+ observation.at - previous.at <= GAP_MS
48
+ )
49
+ basis = previous;
50
+ else if (observation.lane) {
51
+ const own = this.lanes.get(`${key}/${observation.lane}`);
52
+ if (own && observation.at - own.at <= (observation.ttlMs ?? GAP_MS)) basis = own;
53
+ }
54
+ if (observation.lane) this.lanes.set(`${key}/${observation.lane}`, { at: observation.at, prompt });
55
+ if (!basis || basis.prompt < MIN_PROMPT) return;
56
+ const miss = observation.cacheRead < HIT_SHARE * basis.prompt;
57
+ const window = [
58
+ ...(this.windows.get(key) ?? []),
59
+ { miss, lost: miss ? basis.prompt - observation.cacheRead : 0 },
60
+ ].slice(-WINDOW);
61
+ this.windows.set(key, window);
62
+ const misses = window.filter((entry) => entry.miss).length;
63
+ if (misses <= 1) this.warned.delete(key);
64
+ if (misses < MISSES || this.warned.has(key)) return;
65
+ const lost = window.reduce((sum, entry) => sum + entry.lost, 0);
66
+ const message = `Freeflow: the prompt cache for ${observation.model} (${observation.thinking}) missed on ${misses} of the last ${window.length} requests that should have reused it (~${tokens(lost)} tokens re-read). Something is rewriting the start of the prompt or placing it out of the provider's reach; check recently changed extensions or Freeflow settings.`;
67
+ this.warned.set(key, message);
68
+ return message;
69
+ }
70
+ status() {
71
+ return [...this.warned.values()];
72
+ }
73
+ }
@@ -0,0 +1,58 @@
1
+ import { isChatGPTSignIn } from "../routes.js";
2
+ import { planAnchor } from "./anchor.js";
3
+ import { ANTHROPIC_LIMITS, anthropicLayout, applyAnthropicAnchor } from "./anthropic.js";
4
+ import {
5
+ OPENAI_RESPONSES_LIMITS,
6
+ applyResponsesAnchor,
7
+ responsesLayout,
8
+ supportsExplicitBreakpoints,
9
+ } from "./openai-responses.js";
10
+ export { planAnchor } from "./anchor.js";
11
+ const responses = {
12
+ limits: OPENAI_RESPONSES_LIMITS,
13
+ applies: supportsExplicitBreakpoints,
14
+ layout: responsesLayout,
15
+ apply: applyResponsesAnchor,
16
+ };
17
+ // APIs with unbounded prefix lookup need no entry: every prefix they wrote stays reachable. The ChatGPT
18
+ // Codex backend is one: live probes found full prefix reuse after 60 appended message endings, and it
19
+ // rejects prompt_cache_breakpoint, so it must never be adapted. Sign in with ChatGPT on the openai provider
20
+ // reaches the same backend (full reuse after 25 endings, breakpoints rejected) and is skipped in adapt().
21
+ const PROTOCOLS = {
22
+ "anthropic-messages": { limits: ANTHROPIC_LIMITS, layout: anthropicLayout, apply: applyAnthropicAnchor },
23
+ "openai-responses": responses,
24
+ "azure-openai-responses": responses,
25
+ };
26
+ /** Recent requests per session and model; enough to find the requester's own earlier entry. */
27
+ const REMEMBERED = 8;
28
+ export class CacheAnchorAdapter {
29
+ enabled;
30
+ lanes = new Map();
31
+ constructor(enabled = () => true) {
32
+ this.enabled = enabled;
33
+ }
34
+ reset() {
35
+ this.lanes.clear();
36
+ }
37
+ adapt(payload, ctx) {
38
+ const model = ctx?.model;
39
+ const protocol = model && PROTOCOLS[model.api];
40
+ if (!protocol || (protocol.applies && !protocol.applies(model)) || !this.enabled()) return payload;
41
+ if (isChatGPTSignIn(model, ctx)) return payload;
42
+ try {
43
+ const session = ctx.sessionManager?.getSessionId?.() ?? "memory";
44
+ const lane = JSON.stringify([session, model.provider, model.api, model.id, model.baseUrl ?? ""]);
45
+ const mapped = protocol.layout(payload);
46
+ if (!mapped) return payload;
47
+ const prior = this.lanes.get(lane) ?? [];
48
+ const plan = planAnchor(prior, mapped.layout, protocol.limits);
49
+ const sent = plan ? protocol.apply(payload, mapped, plan) : payload;
50
+ const remembered = sent === payload ? mapped.layout : protocol.layout(sent).layout;
51
+ this.lanes.set(lane, [remembered, ...prior].slice(0, REMEMBERED));
52
+ return sent;
53
+ } catch {
54
+ // Cache placement is an optimization; never turn it into a failed request.
55
+ return payload;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,210 @@
1
+ import { isChatGPTSignIn } from "../routes.js";
2
+ const responses = {
3
+ cap: (payload) => ({ ...payload, max_output_tokens: 16 }),
4
+ retention: (payload) =>
5
+ payload?.prompt_cache_retention === "24h" || payload?.prompt_cache_options?.ttl ? "long" : undefined,
6
+ };
7
+ const PROTOCOLS = {
8
+ "anthropic-messages": {
9
+ cap: (payload) => ({ ...payload, max_tokens: 1 }),
10
+ retention: (payload) => (JSON.stringify(payload).includes('"ttl":"1h"') ? "long" : "short"),
11
+ // Budget-based thinking must stay below max_tokens and is part of the cache key, so a one-token replay is
12
+ // rejected or writes a different entry. Adaptive thinking has no budget. Pi's own warmer skips these too.
13
+ replayable: (payload) => payload?.thinking?.type !== "enabled",
14
+ },
15
+ "openai-responses": responses,
16
+ "azure-openai-responses": responses,
17
+ };
18
+ /** Continuation is near-certain while a delegated worker runs: the requester resumes on its return. */
19
+ const CONTINUATION = 0.9;
20
+ /** Same threshold as Pi's own warmer: a refresh must be expected to save at least this much. */
21
+ const MINIMUM_SAVINGS = 0.05;
22
+ /** A refresh counts as reuse only when it reads nearly all of the prompt. */
23
+ const REUSE = 0.9;
24
+ const EXPIRY_MARGIN_MS = 10_000;
25
+ /**
26
+ * Keeps a suspended requester's prompt cache warm while it is certain to resume, such as a Coordinator
27
+ * whose delegated worker is still running. Pi's own warmer only keeps the latest request warm, which
28
+ * during a worker run belongs to the worker. Eligibility and economics come from the model's declared
29
+ * cache lifetime and prices, so any provider that declares them is handled the same way.
30
+ */
31
+ export class CacheKeepAlive {
32
+ options;
33
+ lanes = new Map();
34
+ holdSource = () => undefined;
35
+ requester = () => undefined;
36
+ active;
37
+ lastStop;
38
+ now;
39
+ setTimer;
40
+ clearTimer;
41
+ send;
42
+ constructor(options = {}) {
43
+ this.options = options;
44
+ this.now = options.now ?? Date.now;
45
+ this.setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms).unref?.());
46
+ this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
47
+ this.send = options.send ?? sendThroughHost;
48
+ }
49
+ setHoldSource(source) {
50
+ this.holdSource = source;
51
+ }
52
+ /** Names who is sending the current request, so requesters sharing a model keep separate lanes. */
53
+ setRequesterSource(source) {
54
+ this.requester = () => {
55
+ try {
56
+ return source();
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ };
61
+ }
62
+ reset() {
63
+ this.stop("reset");
64
+ this.lanes.clear();
65
+ }
66
+ status() {
67
+ if (this.active)
68
+ return `Keeping ${this.lanes.get(this.active.lane)?.model?.id} cache warm while its worker runs · $${this.active.spent.toFixed(3)} spent`;
69
+ return this.lastStop;
70
+ }
71
+ /** Remember the request just sent on its model; a hold keeps the latest one warm. */
72
+ record(payload, ctx) {
73
+ const key = laneKey(ctx, undefined, this.requester());
74
+ // Sign in with ChatGPT rejects max_output_tokens (live, 400), so a replay cannot be capped and is never sent.
75
+ const protocol = isChatGPTSignIn(ctx.model, ctx) ? undefined : PROTOCOLS[ctx.model?.api];
76
+ if (!key || !protocol) return;
77
+ if (protocol.replayable?.(payload) === false) {
78
+ this.lanes.delete(key);
79
+ if (this.active?.lane === key) this.stop("this request cannot be replayed with a capped output");
80
+ return;
81
+ }
82
+ const prior = this.lanes.get(key);
83
+ this.lanes.set(key, { key, model: ctx.model, payload, sentAt: this.now(), prompt: prior?.prompt });
84
+ if (this.active?.lane === key) this.schedule();
85
+ }
86
+ /** Provider-reported usage of the latest request on a model gives the prompt size to price. */
87
+ observe(ctx, usage, producer) {
88
+ const lane = this.lanes.get(laneKey(ctx, producer, this.requester()) ?? "");
89
+ if (lane && usage) lane.prompt = usage.input + usage.cacheRead + usage.cacheWrite;
90
+ }
91
+ /** Start or stop holding according to the current run and hold source. */
92
+ evaluate(ctx) {
93
+ const hold = this.currentHold(ctx);
94
+ const key = hold && laneKey(ctx, hold, hold.requester);
95
+ if (this.active && this.active.lane === key) {
96
+ this.active.ctx = ctx;
97
+ return;
98
+ }
99
+ this.stop(hold ? "hold changed" : "no suspended requester");
100
+ if (!key || !this.lanes.has(key)) return;
101
+ this.active = { lane: key, since: this.now(), spent: 0, ctx };
102
+ this.schedule();
103
+ }
104
+ currentHold(ctx) {
105
+ if (!(this.options.enabled?.() ?? true) || ctx?.isIdle?.() !== false) return;
106
+ try {
107
+ return this.holdSource();
108
+ } catch {
109
+ return;
110
+ }
111
+ }
112
+ stop(reason) {
113
+ if (this.active?.timer !== undefined) this.clearTimer(this.active.timer);
114
+ if (this.active) this.lastStop = `Cache keep-alive stopped: ${reason}`;
115
+ this.active = undefined;
116
+ }
117
+ ttlMs(lane) {
118
+ const protocol = PROTOCOLS[lane.model.api];
119
+ const env = this.options.env ?? process.env;
120
+ const retention = protocol?.retention(lane.payload) ?? (env.PI_CACHE_RETENTION === "long" ? "long" : "short");
121
+ const seconds = lane.model.promptCache?.[retention];
122
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1000 : undefined;
123
+ }
124
+ schedule() {
125
+ const active = this.active;
126
+ if (!active) return;
127
+ if (active.timer !== undefined) this.clearTimer(active.timer);
128
+ active.timer = undefined;
129
+ const lane = this.lanes.get(active.lane);
130
+ const ttl = this.ttlMs(lane);
131
+ if (!ttl || ttl <= EXPIRY_MARGIN_MS) return this.stop("model declares no cache lifetime");
132
+ const at = lane.sentAt + refreshDelay(ttl);
133
+ active.timer = this.setTimer(() => this.refresh(active), Math.max(0, at - this.now()));
134
+ }
135
+ async refresh(active) {
136
+ if (this.active !== active) return;
137
+ active.timer = undefined;
138
+ const lane = this.lanes.get(active.lane);
139
+ const hold = this.currentHold(active.ctx);
140
+ if (!hold || laneKey(active.ctx, hold, hold.requester) !== active.lane)
141
+ return this.stop("requester resumed or run ended");
142
+ const now = this.now();
143
+ // Like Pi's warmer, a refresh later than halfway from its slot to expiry is likely a full rewrite.
144
+ const ttl = this.ttlMs(lane),
145
+ delay = refreshDelay(ttl);
146
+ if (now > lane.sentAt + delay + (ttl - delay) / 2) return this.stop("cache entry already expired");
147
+ if (now - active.since > (this.options.maxHoldMs ?? 6 * 3_600_000)) return this.stop("hold duration limit");
148
+ const economics = price(lane);
149
+ if (!economics) return this.stop("prompt size or prices unknown");
150
+ if (CONTINUATION * economics.miss - economics.warm < MINIMUM_SAVINGS)
151
+ return this.stop("refresh would not save money");
152
+ if (active.spent + economics.warm > (this.options.maxHoldCost ?? 2)) return this.stop("hold spend limit");
153
+ let usage;
154
+ try {
155
+ usage = await this.send(lane.model, PROTOCOLS[lane.model.api].cap(lane.payload), active.ctx);
156
+ } catch {
157
+ usage = undefined;
158
+ }
159
+ if (this.active !== active) return;
160
+ if (!usage) return this.stop("refresh failed");
161
+ const spent = cost(lane.model, usage);
162
+ active.spent += spent;
163
+ try {
164
+ this.options.onRefresh?.({ provider: lane.model.provider, model: lane.model.id, usage, cost: spent });
165
+ } catch {
166
+ // Attribution is diagnostic and never changes warming.
167
+ }
168
+ if (usage.cacheRead < REUSE * lane.prompt || usage.cacheWrite > (1 - REUSE) * lane.prompt)
169
+ return this.stop("refresh did not reuse the cache");
170
+ lane.sentAt = now;
171
+ this.schedule();
172
+ }
173
+ }
174
+ /** Pi's refresh point: 90% of the lifetime, keeping at least ten seconds of margin. */
175
+ const refreshDelay = (ttl) => Math.min(ttl * 0.9, ttl - EXPIRY_MARGIN_MS);
176
+ function laneKey(ctx, hold, requester) {
177
+ const provider = hold?.provider ?? ctx?.model?.provider,
178
+ model = hold?.modelId ?? ctx?.model?.id;
179
+ if (!provider || !model) return;
180
+ return JSON.stringify([ctx?.sessionManager?.getSessionId?.() ?? "memory", provider, model, requester ?? ""]);
181
+ }
182
+ const perToken = (value) => (typeof value === "number" ? value / 1_000_000 : undefined);
183
+ function price(lane) {
184
+ const c = lane.model.cost ?? {};
185
+ const read = perToken(c.cacheRead),
186
+ input = perToken(c.input),
187
+ output = perToken(c.output) ?? 0;
188
+ const write = perToken(c.cacheWrite) || input;
189
+ if (!lane.prompt || read === undefined || write === undefined) return;
190
+ return { warm: lane.prompt * read + output, miss: Math.max(0, lane.prompt * (write - read)) };
191
+ }
192
+ function cost(model, usage) {
193
+ const c = model.cost ?? {};
194
+ return (
195
+ usage.input * (perToken(c.input) ?? 0) +
196
+ usage.output * (perToken(c.output) ?? 0) +
197
+ usage.cacheRead * (perToken(c.cacheRead) ?? 0) +
198
+ usage.cacheWrite * (perToken(c.cacheWrite) ?? 0)
199
+ );
200
+ }
201
+ /** Replay through the host's own transport so provider auth and request shaping apply as usual. */
202
+ async function sendThroughHost(model, payload, ctx) {
203
+ const stream = ctx.modelRegistry.streamSimple(
204
+ model,
205
+ { systemPrompt: "", messages: [{ role: "user", content: "cache keep-alive", timestamp: Date.now() }] },
206
+ { onPayload: () => payload, maxRetries: 0, maxTokens: 1 },
207
+ );
208
+ const message = await stream.result();
209
+ return message.stopReason === "error" || message.stopReason === "aborted" ? undefined : message.usage;
210
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Prompt-cache diagnostics for /freeflow status. The footer only shows current Freeflow settings, so
3
+ * cache mechanics (keep-alive, context replay, effort baselines) report here instead.
4
+ */
5
+ export class CacheMonitor {
6
+ entries = new Map();
7
+ set(key, text) {
8
+ if (text) this.entries.set(key, text);
9
+ else this.entries.delete(key);
10
+ }
11
+ lines() {
12
+ return [...this.entries.values()];
13
+ }
14
+ reset() {
15
+ this.entries.clear();
16
+ }
17
+ }
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * OpenAI Responses on GPT-5.6+: the implicit breakpoint sits at the latest eligible message, and a
4
+ * lookup checks it, up to 20 earlier eligible message endings, and the latest explicit breakpoints.
5
+ * A position here is an eligible message ending: a user message, or the last tool output of a group.
6
+ */
7
+ export const OPENAI_RESPONSES_LIMITS = { maxBreakpoints: 50, lookback: 20, margin: 5 };
8
+ const OUTPUTS = new Set(["function_call_output", "custom_tool_call_output"]);
9
+ const withoutMarkers = (value) =>
10
+ JSON.stringify(value, (key, v) => (key === "prompt_cache_breakpoint" ? undefined : v));
11
+ /** Only routes whose documented cache protocol includes explicit breakpoints are adapted. */
12
+ export const supportsExplicitBreakpoints = (model) => model?.compat?.supportsExplicitPromptCacheMode === true;
13
+ export function responsesLayout(payload) {
14
+ if (!payload || !Array.isArray(payload.input)) return;
15
+ const chain = [],
16
+ handles = [],
17
+ breakpoints = [];
18
+ let hash = createHash("sha256")
19
+ .update(withoutMarkers([payload.instructions ?? null, payload.tools ?? null]))
20
+ .digest("hex");
21
+ let pending = "";
22
+ const input = payload.input;
23
+ input.forEach((item, i) => {
24
+ pending += withoutMarkers(item);
25
+ const user = item?.role === "user";
26
+ const lastOutput = OUTPUTS.has(item?.type) && !OUTPUTS.has(input[i + 1]?.type);
27
+ if (!user && !lastOutput) return;
28
+ hash = createHash("sha256").update(hash).update(pending).digest("hex");
29
+ pending = "";
30
+ const content = Array.isArray(item.content) ? item.content : undefined;
31
+ if (content?.some((block) => block?.prompt_cache_breakpoint)) breakpoints.push(chain.length);
32
+ chain.push(hash);
33
+ // Breakpoints go on message content blocks; tool outputs have no documented place for one.
34
+ handles.push(user && content?.length ? { item: i, block: content.length - 1 } : undefined);
35
+ });
36
+ // The implicit breakpoint: the latest eligible message ending.
37
+ if (chain.length && breakpoints.at(-1) !== chain.length - 1) breakpoints.push(chain.length - 1);
38
+ return { layout: { chain, breakpoints, yieldable: [] }, handles };
39
+ }
40
+ export function applyResponsesAnchor(payload, { handles }, plan) {
41
+ const handle = handles[plan.add];
42
+ if (!handle) return payload;
43
+ const input = [...payload.input];
44
+ const item = { ...input[handle.item], content: [...input[handle.item].content] };
45
+ item.content[handle.block] = { ...item.content[handle.block], prompt_cache_breakpoint: { mode: "explicit" } };
46
+ input[handle.item] = item;
47
+ return { ...payload, input };
48
+ }
@@ -0,0 +1,13 @@
1
+ import { effortHistoryRoute } from "./openai/adapter.js";
2
+ import { isChatGPTSignIn } from "./routes.js";
3
+ /**
4
+ * Whether changing effort on this route keeps its prompt cache. Freeflow's effort history keeps it on qualified
5
+ * GPT-6 routes; Pi keeps it natively on Claude models that take per-message effort, where a request-level
6
+ * effort change would otherwise invalidate every cached message.
7
+ */
8
+ export function keepsCacheAcrossEffort(model, ctx) {
9
+ // Sign in with ChatGPT rejects effort-history items, and a plain effort change rereads the context (live, S-006).
10
+ if (isChatGPTSignIn(model, ctx)) return false;
11
+ if (effortHistoryRoute(model) !== undefined) return true;
12
+ return model?.api === "anthropic-messages" && model.compat?.supportsMidConvoEffort === true;
13
+ }
@@ -1,4 +1,45 @@
1
- import { registerAstraSupport } from "./astra/index.js";
2
- export function registerProviderSupport(pi) {
3
- registerAstraSupport(pi);
1
+ import { CacheAnchorAdapter } from "./cache/index.js";
2
+ import { CacheKeepAlive } from "./cache/keepalive.js";
3
+ import { registerOpenAIEffortSupport } from "./openai/index.js";
4
+ export function registerProviderSupport(pi, enabled, monitor) {
5
+ registerOpenAIEffortSupport(pi, enabled, monitor);
6
+ const anchor = new CacheAnchorAdapter(enabled);
7
+ const report = () => monitor?.set("keep-alive", keepAlive.status());
8
+ const keepAlive = new CacheKeepAlive({
9
+ enabled,
10
+ onRefresh: (record) => {
11
+ try {
12
+ // Pi's session usage totals are host-owned; keep-alive spend is recorded for Freeflow attribution.
13
+ pi.appendEntry?.("freeflow-cache-keepalive-v1", { ...record, at: Date.now() });
14
+ } catch {}
15
+ report();
16
+ },
17
+ });
18
+ const evaluate = (ctx) => {
19
+ keepAlive.evaluate(ctx);
20
+ report();
21
+ };
22
+ pi.on("session_shutdown", () => {
23
+ anchor.reset();
24
+ keepAlive.reset();
25
+ report();
26
+ });
27
+ pi.on("before_provider_request", (event, ctx) => {
28
+ const sent = anchor.adapt(event.payload, ctx);
29
+ try {
30
+ keepAlive.record(sent, ctx);
31
+ evaluate(ctx);
32
+ } catch {
33
+ // Keep-alive bookkeeping never affects the request being sent.
34
+ }
35
+ return sent;
36
+ });
37
+ pi.on("message_end", (event, ctx) => {
38
+ const message = event?.message;
39
+ if (message?.role === "assistant" && message.provider && message.model)
40
+ keepAlive.observe(ctx, message.usage, { provider: message.provider, modelId: message.model });
41
+ });
42
+ pi.on("agent_start", (_event, ctx) => evaluate(ctx));
43
+ pi.on("agent_end", (_event, ctx) => evaluate(ctx));
44
+ return { keepAlive };
4
45
  }