@hassangameryt/freeflow 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) 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 +151 -136
  7. package/capabilities/cognitive-routing/SKILL.md +110 -96
  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 +40 -21
  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/cognitive-routing/event-store.js +280 -0
  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/projection.js +503 -0
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +31 -10
  31. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  32. package/pi-extension/dist/cognitive-routing/schemas.js +209 -0
  33. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  34. package/pi-extension/dist/cognitive-routing/sources.js +291 -0
  35. package/pi-extension/dist/cognitive-routing/state.js +487 -0
  36. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  37. package/pi-extension/dist/cognitive-routing/tools.js +60 -0
  38. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +10 -12
  39. package/pi-extension/dist/compaction/carry.js +191 -0
  40. package/pi-extension/dist/compaction/config.js +22 -0
  41. package/pi-extension/dist/compaction/controller.js +471 -0
  42. package/pi-extension/dist/compaction/harness.js +77 -0
  43. package/pi-extension/dist/compaction/results.js +158 -0
  44. package/pi-extension/dist/compaction/thresholds.js +35 -0
  45. package/pi-extension/dist/compaction/tool.js +57 -0
  46. package/pi-extension/dist/host/branch.js +13 -0
  47. package/pi-extension/dist/host/canonical.js +12 -0
  48. package/pi-extension/dist/host/catalog.js +77 -0
  49. package/pi-extension/dist/host/config.js +374 -0
  50. package/pi-extension/dist/host/projection-tags.js +60 -0
  51. package/pi-extension/dist/host/prompts.js +82 -0
  52. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  53. package/pi-extension/dist/host/request-history.js +298 -0
  54. package/pi-extension/dist/host/runtime-state.js +189 -0
  55. package/pi-extension/dist/host/settings/freeflow-command.js +1570 -0
  56. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  57. package/pi-extension/dist/host/staging.js +28 -0
  58. package/pi-extension/dist/host/status.js +101 -0
  59. package/pi-extension/dist/index.js +441 -170
  60. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  61. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  62. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  63. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  64. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  65. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  66. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  67. package/pi-extension/dist/provider-support/effort.js +13 -0
  68. package/pi-extension/dist/provider-support/index.js +45 -0
  69. package/pi-extension/dist/provider-support/openai/adapter.js +127 -0
  70. package/pi-extension/dist/provider-support/openai/history.js +127 -0
  71. package/pi-extension/dist/provider-support/openai/index.js +15 -0
  72. package/pi-extension/dist/provider-support/openai/session-state.js +105 -0
  73. package/pi-extension/dist/provider-support/routes.js +17 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  78. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  79. package/pi-extension/dist/tool-execution/background.js +238 -0
  80. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  81. package/pi-extension/dist/tool-execution/config.js +22 -0
  82. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  83. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  84. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  85. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  86. package/pi-extension/dist/tool-execution/tools.js +16 -0
  87. package/pi-extension/freeflow/README.md +9 -0
  88. package/plugin.json +1 -1
  89. package/runtime/prompts/cognitive-routing.md +4 -4
  90. package/runtime/prompts/core.md +22 -75
  91. package/runtime/prompts/interaction-contract.md +15 -8
  92. package/runtime/prompts/tool-execution.md +38 -0
  93. package/runtime/prompts/working-method.md +108 -0
  94. package/skills/action-selection/SKILL.md +39 -47
  95. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  96. package/skills/bypass/SKILL.md +2 -2
  97. package/skills/commit-work/SKILL.md +31 -83
  98. package/skills/commit-work/references/git-state-edges.md +1 -1
  99. package/skills/commit-work/references/simple-push.md +2 -0
  100. package/skills/commit-work/references/staging-decisions.md +1 -1
  101. package/skills/decision-gate/SKILL.md +31 -50
  102. package/skills/design-for-depth/SKILL.md +44 -53
  103. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  104. package/skills/design-for-depth/references/implementation-decisions.md +4 -4
  105. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  106. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  107. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  108. package/skills/diagnose-failure/SKILL.md +39 -24
  109. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  110. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  111. package/skills/discuss/SKILL.md +37 -13
  112. package/skills/discuss/references/discussion-continuity.md +7 -27
  113. package/skills/evaluate-skill/SKILL.md +49 -80
  114. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  115. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  116. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  117. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  118. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  119. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  120. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  121. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  122. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  123. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  124. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  125. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  126. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  127. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  128. package/skills/execute-work/SKILL.md +41 -9
  129. package/skills/execute-work/references/code-practices.md +2 -0
  130. package/skills/execute-work/references/execute-work-edges.md +11 -1
  131. package/skills/handoff/SKILL.md +1 -1
  132. package/skills/handoff/references/handoff-templates.md +1 -1
  133. package/skills/review-artifact/SKILL.md +42 -41
  134. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  135. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  136. package/skills/review-work/SKILL.md +38 -32
  137. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  138. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  139. package/skills/review-work/references/security-risk-lens.md +2 -4
  140. package/skills/setup-freeflow/SKILL.md +3 -1
  141. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  142. package/skills/setup-freeflow/references/host-setup.md +3 -3
  143. package/skills/track-work/SKILL.md +113 -161
  144. package/skills/track-work/references/working-record-format.md +189 -350
  145. package/skills/track-work/scripts/lib/document.mjs +62 -6
  146. package/skills/track-work/scripts/lib/format.mjs +16 -1
  147. package/skills/track-work/scripts/lib/store.mjs +9 -8
  148. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  149. package/skills/track-work/scripts/working-record.mjs +34 -12
  150. package/skills/verify-work/SKILL.md +29 -20
  151. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  152. package/skills/verify-work/references/integration-evidence.md +2 -0
  153. package/skills/verify-work/references/performance-evidence.md +3 -1
  154. package/skills/verify-work/references/test-design.md +3 -1
  155. package/skills/workflow/SKILL.md +65 -21
  156. package/skills/workflow/references/domain-skill-composition.md +5 -3
  157. package/skills/write-docs/SKILL.md +83 -0
  158. package/skills/write-docs/references/documentation-shapes.md +119 -0
  159. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  160. package/skills/write-plan/SKILL.md +40 -40
  161. package/skills/write-plan/references/plan-shapes.md +1 -1
  162. package/skills/write-skill/SKILL.md +107 -147
  163. package/skills/write-skill/references/activation-boundaries.md +2 -0
  164. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  165. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  166. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  167. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  168. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  169. package/skills/write-spec/SKILL.md +34 -41
  170. package/skills/write-spec/references/artifact-standards.md +1 -1
  171. package/skills/write-spec/references/decision-records.md +2 -0
  172. package/skills/write-spec/references/spec-shapes.md +1 -1
  173. package/capabilities/context-virtualization/SKILL.md +0 -103
  174. package/capabilities/conversation-history/SKILL.md +0 -53
  175. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  176. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  177. package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -321
  178. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -1585
  179. package/pi-extension/dist/cognitive-routing-v2/schemas.js +0 -107
  180. package/pi-extension/dist/cognitive-routing-v2/state.js +0 -612
  181. package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -91
  182. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  183. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  184. package/pi-extension/dist/context-virtualization/runtime.js +0 -356
  185. package/pi-extension/dist/context-virtualization/state.js +0 -57
  186. package/pi-extension/dist/context-virtualization/types.js +0 -10
  187. package/pi-extension/dist/conversation-history/passages.js +0 -66
  188. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  189. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  190. package/pi-extension/dist/conversation-history/search.js +0 -56
  191. package/pi-extension/dist/conversation-history/types.js +0 -1
  192. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  193. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  194. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  195. package/pi-extension/dist/freeflow-context/tool.js +0 -313
  196. package/pi-extension/dist/freeflow-context/types.js +0 -12
  197. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  198. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  199. package/pi-extension/dist/runtime/runtime-context.js +0 -735
  200. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  201. package/pi-extension/dist/session-sources/events.js +0 -195
  202. package/pi-extension/dist/session-sources/sources.js +0 -233
  203. package/pi-extension/dist/settings/settings-ui.js +0 -1160
  204. package/pi-extension/dist/ui/text-component.js +0 -57
  205. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  206. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -143
  207. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -365
  208. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -256
  209. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -1634
  210. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -109
  211. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -622
  212. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -93
  213. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -222
  214. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  215. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  216. package/pi-extension/src/context-virtualization/runtime.ts +0 -406
  217. package/pi-extension/src/context-virtualization/state.ts +0 -79
  218. package/pi-extension/src/context-virtualization/types.ts +0 -92
  219. package/pi-extension/src/conversation-history/passages.ts +0 -81
  220. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  221. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  222. package/pi-extension/src/conversation-history/search.ts +0 -79
  223. package/pi-extension/src/conversation-history/types.ts +0 -31
  224. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  225. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  226. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  227. package/pi-extension/src/freeflow-context/tool.ts +0 -365
  228. package/pi-extension/src/freeflow-context/types.ts +0 -70
  229. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  230. package/pi-extension/src/index.ts +0 -368
  231. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  232. package/pi-extension/src/runtime/runtime-context.ts +0 -838
  233. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  234. package/pi-extension/src/session-sources/events.ts +0 -208
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -263
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1422
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -484
  241. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  242. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -441
  243. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  244. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -245
  245. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  246. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -236
  247. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -230
  248. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -153
  249. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  250. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -275
  251. package/pi-extension/tests/context-virtualization/extension.test.js +0 -765
  252. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  253. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -216
  254. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  255. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  256. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  257. package/pi-extension/tests/fixtures/routing-native.js +0 -188
  258. package/pi-extension/tests/integration/mode-free-core.test.js +0 -219
  259. package/pi-extension/tests/integration/pi-extension.test.js +0 -968
  260. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -431
  261. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  262. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  263. package/pi-extension/tsconfig.json +0 -16
  264. package/runtime/prompts/context-virtualization.md +0 -3
  265. package/runtime/prompts/conversation-history.md +0 -3
  266. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  267. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
@@ -0,0 +1,374 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { resolveCognitiveRoutingState, supportsCognitiveRoutingModelRegistry } from "../cognitive-routing/config.js";
4
+ import { sessionStage, stagedFor } from "./staging.js";
5
+ import { resolveToolExecutionConfig, validateToolExecutionConfig } from "../tool-execution/config.js";
6
+ import { resolveCompactionConfig, validateCompactionConfig } from "../compaction/config.js";
7
+ import { cachedBranch } from "./branch.js";
8
+ /**
9
+ * Freeflow configuration: the repository and personal config files, their layering, per-session core overrides, and
10
+ * the resulting capability state, with optional capabilities disabled inside subagents.
11
+ */
12
+ /**
13
+ * Settings a session may override, as dotted config paths. "enabled" is Freeflow's master switch; the others switch a
14
+ * capability and are applied as a layer above the personal config.
15
+ */
16
+ export const SESSION_OVERRIDE_KEYS = [
17
+ "enabled",
18
+ "cognitiveRouting.enabled",
19
+ "cognitiveRouting.projection",
20
+ "toolExecution.enabled",
21
+ "compaction.enabled",
22
+ "compaction.carry",
23
+ ];
24
+ const SESSION_OVERRIDES_ENTRY = "freeflow-session-overrides";
25
+ const SESSION_CORE_KEYS = new Set(SESSION_OVERRIDE_KEYS);
26
+ let currentSessionOverrides = {};
27
+ // pi-subagents stamps child system prompts with this tag before binding extensions.
28
+ // Keeping detection at the prompt boundary avoids a process-global child flag.
29
+ const SUBAGENT_AGENT_TAG = /<active_agent\s+name="[^"]+"\s*\/>/;
30
+ const FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER = "<!-- freeflow-subagent-capabilities: disabled -->";
31
+ const SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE = "Optional Freeflow capabilities are disabled for subagents.";
32
+ function isSubagentContext(context) {
33
+ try {
34
+ const systemPrompt = context?.getSystemPrompt?.();
35
+ return (
36
+ typeof systemPrompt === "string" &&
37
+ (SUBAGENT_AGENT_TAG.test(systemPrompt) || systemPrompt.includes(FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER))
38
+ );
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function disableSubagentCapability(capability) {
44
+ return {
45
+ ...capability,
46
+ enabled: false,
47
+ effective: false,
48
+ blockingReason: { code: "disabled", message: SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE },
49
+ };
50
+ }
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function removedContextConfigError(value, filePath) {
55
+ const removedKeys = ["contextVirtualization", "conversationHistory"].filter((key) =>
56
+ Object.prototype.hasOwnProperty.call(value, key),
57
+ );
58
+ return removedKeys.length > 0
59
+ ? `Removed Freeflow context setting(s) in ${filePath}: ${removedKeys.join(", ")}. Delete those keys before using Freeflow.`
60
+ : null;
61
+ }
62
+ function validateCoreConfigFields(value) {
63
+ if (value.enabled !== undefined && typeof value.enabled !== "boolean") {
64
+ return "enabled must be a boolean";
65
+ }
66
+ return null;
67
+ }
68
+ function validateFreeflowConfigShape(value) {
69
+ if (!isRecord(value)) {
70
+ return "config must be a JSON object";
71
+ }
72
+ const removedContextError = removedContextConfigError(value, ".freeflow/config.json");
73
+ if (removedContextError) return removedContextError;
74
+ // outputRouter, observedRouting, and scriptTransform belong to retired features; they are accepted and ignored so
75
+ // existing configs keep loading.
76
+ const allowedKeys = new Set([
77
+ "enabled",
78
+ "outputRouter",
79
+ "observedRouting",
80
+ "scriptTransform",
81
+ "cognitiveRouting",
82
+ "toolExecution",
83
+ "compaction",
84
+ ]);
85
+ for (const key of Object.keys(value)) {
86
+ if (!allowedKeys.has(key)) {
87
+ return `unsupported top-level config key: ${key}`;
88
+ }
89
+ }
90
+ const coreError = validateCoreConfigFields(value);
91
+ if (coreError) return coreError;
92
+ for (const key of ["outputRouter", "observedRouting", "scriptTransform"]) {
93
+ if (value[key] !== undefined && !isRecord(value[key])) {
94
+ return `${key} must be an object`;
95
+ }
96
+ }
97
+ if (value.toolExecution !== undefined) {
98
+ const error = validateToolExecutionConfig(value.toolExecution);
99
+ if (error) return error;
100
+ }
101
+ if (value.compaction !== undefined) {
102
+ const error = validateCompactionConfig(value.compaction);
103
+ if (error) return error;
104
+ }
105
+ return null;
106
+ }
107
+ function validateFreeflowLocalConfigShape(value) {
108
+ if (!isRecord(value)) {
109
+ return "local config must be a JSON object";
110
+ }
111
+ const removedContextError = removedContextConfigError(value, ".freeflow/local.json");
112
+ if (removedContextError) return removedContextError;
113
+ // processing belongs to a retired feature; it is accepted and ignored so existing local configs keep loading.
114
+ const allowedKeys = new Set(["enabled", "processing", "cognitiveRouting", "toolExecution", "compaction"]);
115
+ for (const key of Object.keys(value)) {
116
+ if (!allowedKeys.has(key)) {
117
+ return `unsupported top-level local config key: ${key}`;
118
+ }
119
+ }
120
+ const coreError = validateCoreConfigFields(value);
121
+ if (coreError) return coreError;
122
+ if (value.toolExecution !== undefined) {
123
+ const error = validateToolExecutionConfig(value.toolExecution);
124
+ if (error) return error;
125
+ }
126
+ if (value.compaction !== undefined) return validateCompactionConfig(value.compaction);
127
+ return null;
128
+ }
129
+ async function readConfigFileState(path, validate) {
130
+ try {
131
+ const raw = await readFile(path, "utf8");
132
+ const parsed = JSON.parse(raw);
133
+ const validationError = validate(parsed);
134
+ if (validationError) {
135
+ return {
136
+ path,
137
+ exists: true,
138
+ valid: false,
139
+ parsed: {},
140
+ parseError: validationError,
141
+ };
142
+ }
143
+ return { path, exists: true, valid: true, parsed, parseError: null };
144
+ } catch (error) {
145
+ const code = error && typeof error === "object" ? error.code : undefined;
146
+ if (code === "ENOENT") {
147
+ return {
148
+ path,
149
+ exists: false,
150
+ valid: false,
151
+ parsed: {},
152
+ parseError: null,
153
+ };
154
+ }
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ return {
157
+ path,
158
+ exists: true,
159
+ valid: false,
160
+ parsed: {},
161
+ parseError: message,
162
+ };
163
+ }
164
+ }
165
+ function readFreeflowConfigState(cwd) {
166
+ return readConfigFileState(join(cwd, ".freeflow/config.json"), validateFreeflowConfigShape);
167
+ }
168
+ function readFreeflowLocalConfigState(cwd) {
169
+ return readConfigFileState(join(cwd, ".freeflow/local.json"), validateFreeflowLocalConfigShape);
170
+ }
171
+ export async function readFreeflowConfig(cwd) {
172
+ const state = await readFreeflowConfigState(cwd);
173
+ return state.valid ? state.parsed : {};
174
+ }
175
+ export async function readFreeflowLocalConfig(cwd) {
176
+ const state = await readFreeflowLocalConfigState(cwd);
177
+ return state.valid ? state.parsed : {};
178
+ }
179
+ function resolveLayeredValue(repository, local, key, fallback) {
180
+ if (Object.hasOwn(local, key)) {
181
+ return { value: local[key], source: "local" };
182
+ }
183
+ if (Object.hasOwn(repository, key)) {
184
+ return { value: repository[key], source: "repository" };
185
+ }
186
+ return { value: fallback, source: "builtin" };
187
+ }
188
+ function resolveCoreConfig(repository, local) {
189
+ const enabled = resolveLayeredValue(repository, local, "enabled", true);
190
+ return {
191
+ config: { enabled: enabled.value },
192
+ sources: { enabled: enabled.source },
193
+ };
194
+ }
195
+ function normalizeSessionOverrides(value) {
196
+ if (!isRecord(value)) return {};
197
+ const overrides = {};
198
+ for (const key of SESSION_CORE_KEYS) {
199
+ if (typeof value[key] === "boolean") {
200
+ overrides[key] = value[key];
201
+ }
202
+ }
203
+ return overrides;
204
+ }
205
+ /** The capability overrides of this session as a config layer, e.g. { compaction: { carry: false } }. */
206
+ function sessionConfigLayer(overrides) {
207
+ const layer = {};
208
+ for (const [key, value] of Object.entries(overrides)) {
209
+ const [section, field] = key.split(".");
210
+ if (!field || typeof value !== "boolean") continue;
211
+ layer[section] = { ...(layer[section] ?? {}), [field]: value };
212
+ }
213
+ return layer;
214
+ }
215
+ function withSessionLayer(local, session) {
216
+ const merged = { ...local };
217
+ for (const [section, fields] of Object.entries(session))
218
+ merged[section] = { ...(isRecord(local[section]) ? local[section] : {}), ...fields };
219
+ return merged;
220
+ }
221
+ function resolveSessionCoreConfig(layers) {
222
+ const configured = layers.coreConfig;
223
+ const sources = layers.sources;
224
+ const enabled = currentSessionOverrides.enabled;
225
+ return {
226
+ config: { enabled: typeof enabled === "boolean" ? enabled : configured.enabled },
227
+ sources: { enabled: typeof enabled === "boolean" ? "session" : sources.enabled },
228
+ };
229
+ }
230
+ export async function readFreeflowConfigLayers(cwd) {
231
+ const [repository, local] = await Promise.all([readFreeflowConfigState(cwd), readFreeflowLocalConfigState(cwd)]);
232
+ const repositoryConfig = repository.valid ? repository.parsed : {};
233
+ const localConfig = local.valid ? local.parsed : {};
234
+ const core = resolveCoreConfig(repositoryConfig, localConfig);
235
+ const localValid = !local.exists || local.valid;
236
+ const configured = repository.valid && localValid;
237
+ let blockingState = null;
238
+ if (!repository.valid) {
239
+ blockingState = repository;
240
+ } else if (local.exists && !local.valid) {
241
+ blockingState = local;
242
+ }
243
+ return {
244
+ configured,
245
+ repositoryConfigured: repository.valid,
246
+ repository,
247
+ local,
248
+ coreConfig: core.config,
249
+ sources: core.sources,
250
+ blockingConfigPath: blockingState?.path ?? null,
251
+ parseError: blockingState?.parseError ?? null,
252
+ };
253
+ }
254
+ export async function readCapabilityState(cwd, host = undefined) {
255
+ const layers = await readFreeflowConfigLayers(cwd);
256
+ const effectiveCore = resolveSessionCoreConfig(layers);
257
+ const enabled = layers.configured && effectiveCore.config.enabled;
258
+ const subagentContext = isSubagentContext(host);
259
+ const hostSupportsCognitiveRouting = !subagentContext && supportsCognitiveRoutingModelRegistry(host);
260
+ const sessionLayer = sessionConfigLayer(currentSessionOverrides);
261
+ const configuredCognitiveRouting = await resolveCognitiveRoutingState(
262
+ layers.repository.parsed,
263
+ layers.local.parsed,
264
+ hostSupportsCognitiveRouting ? host : undefined,
265
+ sessionLayer,
266
+ );
267
+ const disabledReason = enabled ? undefined : { code: "disabled", message: "Freeflow is disabled" };
268
+ const cognitiveRouting = enabled
269
+ ? configuredCognitiveRouting
270
+ : {
271
+ ...configuredCognitiveRouting,
272
+ enabled: false,
273
+ effective: false,
274
+ blockingReason: disabledReason,
275
+ };
276
+ const repositoryConfig = layers.repository.valid ? layers.repository.parsed : {};
277
+ const localConfig = layers.local.valid ? layers.local.parsed : {};
278
+ const sessionLocalConfig = withSessionLayer(localConfig, sessionLayer);
279
+ const toolExecution = resolveToolExecutionConfig(repositoryConfig, sessionLocalConfig, enabled);
280
+ const compaction = resolveCompactionConfig(repositoryConfig, sessionLocalConfig, enabled);
281
+ const capabilityState = {
282
+ configured: layers.configured,
283
+ repositoryConfigured: layers.repositoryConfigured,
284
+ configExists: layers.repository.exists,
285
+ configValid: layers.configured,
286
+ configPath: layers.blockingConfigPath ?? layers.repository.path,
287
+ parseError: layers.parseError,
288
+ localConfigExists: layers.local.exists,
289
+ localConfigValid: !layers.local.exists || layers.local.valid,
290
+ localConfigPath: layers.local.path,
291
+ localConfigParseError: layers.local.parseError,
292
+ configuredCoreConfig: layers.coreConfig,
293
+ configuredSources: layers.sources,
294
+ sessionOverrides: { ...currentSessionOverrides },
295
+ configSources: effectiveCore.sources,
296
+ enabled,
297
+ hostSupportsCognitiveRouting,
298
+ cognitiveRouting,
299
+ toolExecution,
300
+ compaction,
301
+ };
302
+ if (!subagentContext) return capabilityState;
303
+ return {
304
+ ...capabilityState,
305
+ cognitiveRouting: disableSubagentCapability(capabilityState.cognitiveRouting),
306
+ toolExecution: disableSubagentCapability(capabilityState.toolExecution),
307
+ compaction: disableSubagentCapability(capabilityState.compaction),
308
+ };
309
+ }
310
+ function recordedSessionOverrides(sessionManager) {
311
+ let recorded = {};
312
+ for (const entry of sessionManager?.getBranch ? cachedBranch(sessionManager) : (sessionManager?.getEntries?.() ?? []))
313
+ if (entry.type === "custom" && entry.customType === SESSION_OVERRIDES_ENTRY)
314
+ recorded = normalizeSessionOverrides(entry.data?.overrides);
315
+ return recorded;
316
+ }
317
+ export function restoreSessionOverrides(ctx) {
318
+ const staged = stagedFor(ctx.sessionManager)?.overrides;
319
+ currentSessionOverrides = normalizeSessionOverrides(staged ?? recordedSessionOverrides(ctx.sessionManager));
320
+ }
321
+ /** Session overrides change nothing until the next prompt, so they are staged and written then. */
322
+ function recordSessionOverrides(ctx, pi) {
323
+ const stage = sessionStage(ctx?.sessionManager);
324
+ if (stage) stage.overrides = { ...currentSessionOverrides };
325
+ else pi?.appendEntry?.(SESSION_OVERRIDES_ENTRY, { overrides: { ...currentSessionOverrides } });
326
+ }
327
+ /** Write staged session overrides at the start of a prompt, unless they equal what the session recorded. */
328
+ export function writeStagedSessionOverrides(overrides, pi, sessionManager) {
329
+ if (!overrides) return;
330
+ const next = normalizeSessionOverrides(overrides);
331
+ if (JSON.stringify(next) === JSON.stringify(recordedSessionOverrides(sessionManager))) return;
332
+ pi?.appendEntry?.(SESSION_OVERRIDES_ENTRY, { overrides: { ...next } });
333
+ }
334
+ export async function setSessionCoreOverride(key, value, ctx, pi) {
335
+ if (!SESSION_CORE_KEYS.has(key)) {
336
+ throw new Error(`Invalid Freeflow session override: ${String(key)}`);
337
+ }
338
+ if (value !== null && typeof value !== "boolean") {
339
+ throw new Error(`Invalid Freeflow session override value for ${key}: ${String(value)}`);
340
+ }
341
+ const hasOverride = Object.hasOwn(currentSessionOverrides, key);
342
+ if ((value === null && !hasOverride) || (value !== null && hasOverride && currentSessionOverrides[key] === value)) {
343
+ return { changed: false, sessionOverrides: { ...currentSessionOverrides } };
344
+ }
345
+ const next = { ...currentSessionOverrides };
346
+ if (value === null) {
347
+ delete next[key];
348
+ } else {
349
+ next[key] = value;
350
+ }
351
+ currentSessionOverrides = next;
352
+ recordSessionOverrides(ctx, pi);
353
+ return {
354
+ changed: true,
355
+ // Every session override switches Freeflow or a capability, which changes tools and guidance: reload.
356
+ reloadRequired: true,
357
+ sessionOverrides: { ...currentSessionOverrides },
358
+ capabilityState: await readCapabilityState(ctx.cwd, ctx),
359
+ };
360
+ }
361
+ export async function resetSessionOverrides(ctx, pi) {
362
+ const hadCoreOverrides = Object.keys(currentSessionOverrides).length > 0;
363
+ const reloadRequired = hadCoreOverrides;
364
+ if (hadCoreOverrides) {
365
+ currentSessionOverrides = {};
366
+ recordSessionOverrides(ctx, pi);
367
+ }
368
+ return {
369
+ changed: hadCoreOverrides,
370
+ reloadRequired,
371
+ sessionOverrides: { ...currentSessionOverrides },
372
+ capabilityState: await readCapabilityState(ctx.cwd, ctx),
373
+ };
374
+ }
@@ -0,0 +1,60 @@
1
+ const projectedEntryIds = new WeakMap();
2
+ // Messages that came from an aligned projection; anything else in the request was generated after it.
3
+ const projectedMessages = new WeakSet();
4
+ // Projected messages whose body a host edit replaced; only these still need content association.
5
+ const editedMessages = new WeakSet();
6
+ /** The unedited native entry behind a request message, when Pi's projection established it. */
7
+ export function projectedEntryId(message) {
8
+ return message && typeof message === "object" ? projectedEntryIds.get(message) : undefined;
9
+ }
10
+ export function tagProjectedMessages(messages, projection, branch) {
11
+ if (!projection) return;
12
+ let count = 0;
13
+ for (const entry of projection.entries) count += entry.messages.length;
14
+ if (count !== messages.length) return;
15
+ let index = 0;
16
+ for (const entry of projection.entries)
17
+ for (const projected of entry.messages) if (projected?.role !== messages[index++]?.role) return;
18
+ // Host edits change the visible body, so their targets keep exact content association.
19
+ const edited = new Set();
20
+ for (const entry of branch) if (entry.type === "context_edit") edited.add(entry.targetId);
21
+ index = 0;
22
+ for (const entry of projection.entries) {
23
+ const source = entry.sourceEntry;
24
+ for (const _projected of entry.messages) {
25
+ const message = messages[index++];
26
+ if (!message || typeof message !== "object") continue;
27
+ projectedMessages.add(message);
28
+ if (edited.has(source.id)) editedMessages.add(message);
29
+ if (
30
+ entry.messages.length === 1 &&
31
+ (source.type === "message" || source.type === "custom_message") &&
32
+ !edited.has(source.id)
33
+ )
34
+ projectedEntryIds.set(message, source.id);
35
+ }
36
+ }
37
+ }
38
+ // Messages Freeflow renders from an entry (routing's views): the entry and the rendering variant, so a consumer can
39
+ // fingerprint each rendering once. Kept apart from projectedEntryIds, which names only unedited native messages.
40
+ const derivedIdentities = new WeakMap();
41
+ /** Mark a message rendered from a native entry with a stable identity: `<entry id>#<variant>` or another unique key. */
42
+ export function tagDerived(message, identity) {
43
+ derivedIdentities.set(message, identity);
44
+ }
45
+ /**
46
+ * A stable identity for a request message whose content never changes under that identity: the unedited native entry
47
+ * behind it, or the identity of a rendering. Undefined for generated or host-edited messages.
48
+ */
49
+ export function occurrenceIdentity(message) {
50
+ if (!message || typeof message !== "object") return undefined;
51
+ return derivedIdentities.get(message) ?? projectedEntryIds.get(message);
52
+ }
53
+ /** Whether a request message came from an aligned Pi projection. */
54
+ export function isProjected(message) {
55
+ return !!message && typeof message === "object" && projectedMessages.has(message);
56
+ }
57
+ /** Whether a host edit replaced the body of a projected request message. */
58
+ export function isHostEdited(message) {
59
+ return !!message && typeof message === "object" && editedMessages.has(message);
60
+ }
@@ -0,0 +1,82 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /**
3
+ * Freeflow's prompt files and the fixed system section built from them. The section never changes with feature state
4
+ * so it stays cached; the Runtime State tells the model what is active.
5
+ */
6
+ let runtimeContextCache = null;
7
+ export function isPromptAvailable(value) {
8
+ return typeof value === "string" && value.trim().length > 0;
9
+ }
10
+ export function hasUsableMandatoryPrompts(freeflowContext) {
11
+ return (
12
+ isPromptAvailable(freeflowContext?.corePrompt) && isPromptAvailable(freeflowContext?.interactionContractPrompt)
13
+ );
14
+ }
15
+ async function readPromptFile(url) {
16
+ try {
17
+ const prompt = await readFile(url, "utf8");
18
+ return isPromptAvailable(prompt) ? prompt.trim() : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function loadRuntimeContext(capabilityState = undefined) {
24
+ const freeflowEnabled = capabilityState?.enabled === true;
25
+ const cognitiveRoutingEnabled = capabilityState?.cognitiveRouting?.effective === true;
26
+ const [corePrompt, interactionContractPrompt, workingMethodPrompt, cognitiveRoutingPrompt, toolExecutionPrompt] =
27
+ await Promise.all([
28
+ freeflowEnabled
29
+ ? readPromptFile(new URL("../../../runtime/prompts/core.md", import.meta.url))
30
+ : Promise.resolve(null),
31
+ freeflowEnabled
32
+ ? readPromptFile(new URL("../../../runtime/prompts/interaction-contract.md", import.meta.url))
33
+ : Promise.resolve(null),
34
+ freeflowEnabled
35
+ ? readPromptFile(new URL("../../../runtime/prompts/working-method.md", import.meta.url))
36
+ : Promise.resolve(null),
37
+ cognitiveRoutingEnabled
38
+ ? readPromptFile(new URL("../../../runtime/prompts/cognitive-routing.md", import.meta.url))
39
+ : Promise.resolve(null),
40
+ capabilityState?.toolExecution?.effective === true
41
+ ? readPromptFile(new URL("../../../runtime/prompts/tool-execution.md", import.meta.url))
42
+ : Promise.resolve(null),
43
+ ]);
44
+ return { corePrompt, interactionContractPrompt, workingMethodPrompt, cognitiveRoutingPrompt, toolExecutionPrompt };
45
+ }
46
+ function runtimeContextCacheSatisfies(capabilityState) {
47
+ if (!runtimeContextCache) return false;
48
+ const expected = {
49
+ corePrompt: capabilityState?.enabled === true,
50
+ interactionContractPrompt: capabilityState?.enabled === true,
51
+ cognitiveRoutingPrompt: capabilityState?.cognitiveRouting?.effective === true,
52
+ };
53
+ return Object.entries(expected).every(([key, required]) => !required || isPromptAvailable(runtimeContextCache[key]));
54
+ }
55
+ export async function refreshRuntimeContext(capabilityState = undefined) {
56
+ runtimeContextCache = await loadRuntimeContext(capabilityState);
57
+ return runtimeContextCache;
58
+ }
59
+ export async function getRuntimeContext(capabilityState = undefined) {
60
+ if (runtimeContextCacheSatisfies(capabilityState)) {
61
+ return runtimeContextCache;
62
+ }
63
+ return refreshRuntimeContext(capabilityState);
64
+ }
65
+ export function stableRuntimeContext(context) {
66
+ const sections = [
67
+ ["Freeflow core", context?.corePrompt],
68
+ ["Freeflow core", context?.interactionContractPrompt],
69
+ ["Working Method", context?.workingMethodPrompt],
70
+ ["Cognitive Routing", context?.cognitiveRoutingPrompt],
71
+ ];
72
+ return [
73
+ "# Freeflow availability contract",
74
+ "Freeflow keeps a fixed reference catalog of its instructions, skills, and tools to preserve prompt caching. Presence in this catalog does not mean a feature is active or an operation is permitted.",
75
+ "Apply the following guidance and Freeflow skills only when the latest extension-generated Freeflow Runtime State marks the corresponding feature active. When Freeflow is inactive, unavailable, or not configured, its core and capability guidance is dormant; do not bootstrap or follow it merely because it is listed. Explicit user instructions retain their normal authority.",
76
+ "For Cognitive Routing, follow the latest control/profile/responsibility snapshots on this branch. Earlier snapshots and notices are historical; they grant no current permission. Tool permissions are checked by the runtime, even though all definitions remain visible. Never call a disabled operation.",
77
+ "Messages that start with `[Freeflow notice, not from the user]` come from Freeflow, not the user. They report facts and harness steps; they are not the user's instructions.",
78
+ ...sections
79
+ .filter(([, text]) => isPromptAvailable(text))
80
+ .map(([feature, text]) => `## Reference guidance: ${feature}\n\n${text.trim()}`),
81
+ ].join("\n\n");
82
+ }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { open, stat } from "node:fs/promises";
2
+ import { statSync } from "node:fs";
3
+ import { lstat, open, stat } from "node:fs/promises";
3
4
  import { TextDecoder } from "node:util";
4
5
  export class ReadOnlySessionError extends Error {
5
6
  code;
@@ -50,7 +51,10 @@ export function parseReadOnlySessionText(text) {
50
51
  const seen = new Set();
51
52
  return lines.map((line, i) => decodeEntry(line, i + 1, seen));
52
53
  }
54
+ /** Whole-file snapshots read so far; tests check that a verified startup reads none. */
55
+ export const readOnlySessionCounters = { snapshots: 0 };
53
56
  export async function readOnlySessionSnapshot(path, options = {}) {
57
+ readOnlySessionCounters.snapshots++;
54
58
  const limits = { ...SESSION_READ_LIMITS, ...options };
55
59
  if (Object.values(limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
56
60
  throw new ReadOnlySessionError("invalid_limit", "Invalid snapshot limit.");
@@ -146,3 +150,110 @@ export function activeReadOnlySessionBranch(snapshot, leafId) {
146
150
  }
147
151
  return path.reverse();
148
152
  }
153
+ // Byte offset and entry count up to which a session file is known to equal Pi's in-memory entries.
154
+ const verified = new Map();
155
+ /** Pi just parsed this file into memory, so its current bytes are the verified prefix. */
156
+ export function trustLoadedSession(reader) {
157
+ const path = reader.getSessionFile?.();
158
+ if (!path) return;
159
+ try {
160
+ verified.set(path, { size: statSync(path).size, count: reader.getEntries().length });
161
+ } catch {
162
+ verified.delete(path);
163
+ }
164
+ }
165
+ /**
166
+ * At `session_start`, trust the file only when Pi just parsed it. A reload keeps Pi's session in memory without
167
+ * re-reading the file, so it establishes nothing: after a failed write, memory could hold an entry the file lacks.
168
+ * The previous trust point, or a full check, still applies after a reload.
169
+ */
170
+ export function trustStartedSession(reason, reader) {
171
+ if (reason !== "reload") trustLoadedSession(reader);
172
+ }
173
+ async function tailMatches(path, reader) {
174
+ const point = verified.get(path);
175
+ if (!point) return false;
176
+ const entries = reader.getEntries();
177
+ let file;
178
+ try {
179
+ file = await open(path, "r");
180
+ const { size } = await file.stat();
181
+ if (size < point.size || entries.length < point.count) return false;
182
+ const buffer = Buffer.alloc(size - point.size);
183
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, point.size);
184
+ if (bytesRead !== buffer.length) return false;
185
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(buffer).split("\n");
186
+ if (lines.pop() !== "") return false;
187
+ const appended = entries.slice(point.count);
188
+ // Pi appends each entry as its JSON serialization, in memory order.
189
+ if (lines.length !== appended.length || lines.some((line, i) => line !== JSON.stringify(appended[i]))) return false;
190
+ verified.set(path, { size, count: entries.length });
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ } finally {
195
+ await file?.close();
196
+ }
197
+ }
198
+ /**
199
+ * Whether every entry Pi holds is persisted, judged only by the bytes appended since the trust point: true when the
200
+ * file grew by exactly those entries. False when there is no trust point or anything differs; callers then fall back
201
+ * to a full snapshot.
202
+ */
203
+ export async function persistedTailMatches(reader) {
204
+ const path = reader.getSessionFile?.();
205
+ return !!path && (await tailMatches(path, reader));
206
+ }
207
+ /** Verify one newly appended native anchor without re-reading the historical session. */
208
+ export async function persistedLastEntryMatches(path, entry) {
209
+ if (!path) return false;
210
+ let file;
211
+ try {
212
+ const expected = Buffer.from(`${JSON.stringify(entry)}\n`, "utf8");
213
+ if (!expected.length || expected.length > 1024 * 1024) return false;
214
+ const named = await lstat(path, { bigint: true });
215
+ if (!named.isFile() || named.isSymbolicLink() || named.size < BigInt(expected.length)) return false;
216
+ file = await open(path, "r");
217
+ const before = await file.stat({ bigint: true });
218
+ if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) return false;
219
+ const actual = Buffer.alloc(expected.length);
220
+ let offset = 0;
221
+ while (offset < actual.length) {
222
+ const read = await file.read(
223
+ actual,
224
+ offset,
225
+ actual.length - offset,
226
+ Number(before.size) - actual.length + offset,
227
+ );
228
+ if (!read.bytesRead) return false;
229
+ offset += read.bytesRead;
230
+ }
231
+ const after = await file.stat({ bigint: true });
232
+ const current = await lstat(path, { bigint: true });
233
+ const signature = (value) => [value.dev, value.ino, value.size, value.mtimeNs, value.ctimeNs].join(":");
234
+ return (
235
+ before.isFile() &&
236
+ signature(named) === signature(before) &&
237
+ signature(before) === signature(after) &&
238
+ signature(after) === signature(current) &&
239
+ actual.equals(expected)
240
+ );
241
+ } catch {
242
+ return false;
243
+ } finally {
244
+ await file?.close();
245
+ }
246
+ }
247
+ /** Whether the persisted session holds exactly this branch: tail-only after a trust point, else a full comparison. */
248
+ export async function persistedBranchMatches(reader, leaf, branch) {
249
+ const path = reader.getSessionFile?.();
250
+ if (!path) return false;
251
+ if (await tailMatches(path, reader)) return true;
252
+ const snapshot = await readOnlySessionSnapshot(path);
253
+ const matches =
254
+ snapshot.sessionId === reader.getSessionId() &&
255
+ JSON.stringify(activeReadOnlySessionBranch(snapshot, leaf)) === JSON.stringify(branch);
256
+ if (matches) trustLoadedSession(reader);
257
+ else verified.delete(path);
258
+ return matches;
259
+ }