@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,388 @@
1
+ import { EFFORTS, PROFILES, DELEGATION_MODES, isObject, requireCondition as check } from "./types.js";
2
+ const text = (x, max = 32768) => typeof x === "string" && x.trim().length > 0 && x.length <= max;
3
+ const identity = (x) => text(x, 512);
4
+ const nullableId = (x) => x === null || identity(x);
5
+ const shape = (x, keys) => isObject(x) && Object.keys(x).every((k) => keys.includes(k));
6
+ const uniqueStrings = (x) => Array.isArray(x) && x.every(identity) && new Set(x).size === x.length;
7
+ const pair = (x) =>
8
+ shape(x, ["provider", "modelId", "thinking"]) &&
9
+ identity(x.provider) &&
10
+ identity(x.modelId) &&
11
+ EFFORTS.includes(x.thinking);
12
+ const profileOverrides = (x) =>
13
+ isObject(x) &&
14
+ Object.keys(x).length > 0 &&
15
+ Object.keys(x).every((key) => PROFILES.includes(key)) &&
16
+ Object.values(x).every((value) => value === null || pair(value));
17
+ const problemList = (xs) =>
18
+ Array.isArray(xs) &&
19
+ xs.every(
20
+ (x) =>
21
+ shape(x, ["ref", "code", "detail"]) && typeof x.ref === "string" && text(x.code, 256) && text(x.detail, 4096),
22
+ );
23
+ const reservation = (r) =>
24
+ shape(r, [
25
+ "id",
26
+ "selectionRevision",
27
+ "receiver",
28
+ "target",
29
+ "qualification",
30
+ "sources",
31
+ "maximumInputTokens",
32
+ "outputReserve",
33
+ "estimateMethod",
34
+ ]) &&
35
+ identity(r.id) &&
36
+ Number.isSafeInteger(r.selectionRevision) &&
37
+ r.selectionRevision >= 0 &&
38
+ PROFILES.includes(r.receiver) &&
39
+ pair(r.target) &&
40
+ text(r.qualification) &&
41
+ Array.isArray(r.sources) &&
42
+ r.sources.every((s) => shape(s, ["ref", "bodyHash"]) && identity(s.ref) && /^[a-f0-9]{64}$/.test(s.bodyHash)) &&
43
+ new Set(r.sources.map((s) => s.ref)).size === r.sources.length &&
44
+ Number.isFinite(r.maximumInputTokens) &&
45
+ r.maximumInputTokens > 0 &&
46
+ Number.isFinite(r.outputReserve) &&
47
+ r.outputReserve >= 0 &&
48
+ text(r.estimateMethod);
49
+ const recovery = (r) => {
50
+ const requestedResults = r?.requestedResults ?? [];
51
+ const results = r?.results ?? [];
52
+ return (
53
+ shape(r, [
54
+ "id",
55
+ "assignmentId",
56
+ "assessmentHandoffId",
57
+ "baseReportRevision",
58
+ "request",
59
+ "requestedPaths",
60
+ "paths",
61
+ "requestedResults",
62
+ "results",
63
+ "state",
64
+ "requestHandoffId",
65
+ "supplementHandoffId",
66
+ "supplementRevision",
67
+ "cancellationReason",
68
+ ]) &&
69
+ identity(r.id) &&
70
+ identity(r.assignmentId) &&
71
+ identity(r.assessmentHandoffId) &&
72
+ Number.isSafeInteger(r.baseReportRevision) &&
73
+ r.baseReportRevision > 0 &&
74
+ text(r.request) &&
75
+ Array.isArray(r.requestedPaths) &&
76
+ r.requestedPaths.length <= 32 &&
77
+ r.requestedPaths.every((path) => text(path, 4096)) &&
78
+ new Set(r.requestedPaths).size === r.requestedPaths.length &&
79
+ Array.isArray(r.paths) &&
80
+ r.paths.length <= 32 &&
81
+ r.paths.every((path) => text(path, 4096)) &&
82
+ new Set(r.paths).size === r.paths.length &&
83
+ Array.isArray(requestedResults) &&
84
+ requestedResults.length <= 32 &&
85
+ requestedResults.every((id) => text(id, 256)) &&
86
+ new Set(requestedResults).size === requestedResults.length &&
87
+ Array.isArray(results) &&
88
+ results.length <= 32 &&
89
+ results.every(
90
+ (grant) =>
91
+ shape(grant, ["id", "sha256"]) &&
92
+ text(grant.id, 256) &&
93
+ typeof grant.sha256 === "string" &&
94
+ /^[a-f0-9]{64}$/.test(grant.sha256),
95
+ ) &&
96
+ new Set(results.map((grant) => grant.id)).size === results.length &&
97
+ ["requested", "reading", "returning", "completed", "cancelled"].includes(r.state) &&
98
+ identity(r.requestHandoffId) &&
99
+ (r.supplementHandoffId === undefined || identity(r.supplementHandoffId)) &&
100
+ Number.isSafeInteger(r.supplementRevision) &&
101
+ r.supplementRevision >= 0 &&
102
+ (r.cancellationReason === undefined || text(r.cancellationReason, 2048))
103
+ );
104
+ };
105
+ const handoff = (h) =>
106
+ shape(h, [
107
+ "id",
108
+ "kind",
109
+ "assignmentId",
110
+ "text",
111
+ "from",
112
+ "to",
113
+ "executionId",
114
+ "toolCallId",
115
+ "basisUserEntryId",
116
+ "state",
117
+ "reportRevision",
118
+ "outcome",
119
+ "limitations",
120
+ "reason",
121
+ ]) &&
122
+ identity(h.id) &&
123
+ identity(h.assignmentId) &&
124
+ text(h.text) &&
125
+ identity(h.executionId) &&
126
+ identity(h.toolCallId) &&
127
+ nullableId(h.basisUserEntryId) &&
128
+ ["delegate", "return", "recovery-request", "recovery-return"].includes(h.kind) &&
129
+ PROFILES.includes(h.from) &&
130
+ PROFILES.includes(h.to) &&
131
+ h.from !== h.to &&
132
+ ["pending", "configured", "blocked", "superseded"].includes(h.state) &&
133
+ Number.isSafeInteger(h.reportRevision) &&
134
+ h.reportRevision >= 0 &&
135
+ Array.isArray(h.limitations) &&
136
+ h.limitations.length <= 32 &&
137
+ h.limitations.every((x) => text(x, 2048)) &&
138
+ (h.reason === undefined || text(h.reason, 4096)) &&
139
+ (h.outcome === undefined || ["completed", "partial", "blocked"].includes(h.outcome)) &&
140
+ (["delegate", "recovery-request"].includes(h.kind)
141
+ ? h.outcome === undefined && h.reportRevision === 0
142
+ : ["completed", "partial", "blocked"].includes(h.outcome) && h.reportRevision > 0);
143
+ const fields = {
144
+ "execution-interrupted": ["executionId", "reason"],
145
+ "assignment-resumed": ["assignmentId", "basisUserEntryId"],
146
+ control: ["control", "profile", "reason"],
147
+ "profile-overrides": ["overrides", "reason"],
148
+ "delegation-override": ["delegation", "reason"],
149
+ "execution-opened": ["execution"],
150
+ "execution-bound": ["executionId", "assistantEntryId", "resultEntryIds", "outcome"],
151
+ "delegate-accepted": ["unit", "assignment", "handoff", "replacement"],
152
+ "return-accepted": ["handoff"],
153
+ "recovery-request-accepted": ["recovery", "handoff"],
154
+ "recovery-supplement-accepted": ["recoveryId", "handoff"],
155
+ "recovery-cancelled": ["recoveryId", "reason"],
156
+ "handoff-retry-requested": [
157
+ "handoffId",
158
+ "attemptId",
159
+ "executionId",
160
+ "toolCallId",
161
+ "reportRevision",
162
+ "selectionRevision",
163
+ ],
164
+ "handoff-prepared": ["handoffId", "reservation"],
165
+ "handoff-state": ["handoffId", "state", "reason", "observedPair"],
166
+ "selection-changed": ["assignmentId", "selection"],
167
+ "assessment-suspended": ["handoffId", "reason", "basisUserEntryId", "problems"],
168
+ "assessment-resumed": ["handoffId", "basisUserEntryId", "reservation"],
169
+ "unit-closed": ["unitId", "outcome", "assessment", "supersededAssignmentId"],
170
+ "sources-exposed": ["executionId", "view", "sources"],
171
+ "request-observed": ["executionId", "profile", "boundary", "manifestHash", "handoffId", "selectionRevision"],
172
+ };
173
+ export function parseRoutingEvent(raw) {
174
+ check(
175
+ isObject(raw) &&
176
+ raw.version === 2 &&
177
+ identity(raw.eventId) &&
178
+ identity(raw.operationId) &&
179
+ identity(raw.stepId) &&
180
+ identity(raw.recordedSessionId) &&
181
+ isObject(raw.data),
182
+ "invalid_event",
183
+ );
184
+ check(
185
+ Object.keys(raw).every((k) =>
186
+ ["version", "eventId", "operationId", "stepId", "recordedSessionId", "data"].includes(k),
187
+ ),
188
+ "invalid_event_field",
189
+ );
190
+ const d = raw.data;
191
+ check(
192
+ typeof d.type === "string" &&
193
+ Object.hasOwn(fields, d.type) &&
194
+ Object.keys(d).every((k) => k === "type" || fields[d.type].includes(k)),
195
+ "invalid_event_type",
196
+ );
197
+ check(Buffer.byteLength(JSON.stringify(raw)) <= 512 * 1024, "event_too_large");
198
+ if (d.handoffId !== undefined) check(identity(d.handoffId), "invalid_handoff_id");
199
+ switch (d.type) {
200
+ case "execution-interrupted":
201
+ check(identity(d.executionId) && text(d.reason, 4096), "invalid_interruption");
202
+ break;
203
+ case "assignment-resumed":
204
+ check(identity(d.assignmentId) && nullableId(d.basisUserEntryId), "invalid_resume");
205
+ break;
206
+ case "control":
207
+ check(
208
+ ["automatic", "manual", "inactive"].includes(d.control) &&
209
+ text(d.reason, 4096) &&
210
+ (d.profile === undefined || PROFILES.includes(d.profile)) &&
211
+ (d.control === "inactive" || PROFILES.includes(d.profile)),
212
+ "invalid_control",
213
+ );
214
+ break;
215
+ case "profile-overrides":
216
+ check(profileOverrides(d.overrides) && text(d.reason, 4096), "invalid_profile_overrides");
217
+ break;
218
+ case "delegation-override":
219
+ check(
220
+ (d.delegation === null || DELEGATION_MODES.includes(d.delegation)) && text(d.reason, 4096),
221
+ "invalid_delegation_override",
222
+ );
223
+ break;
224
+ case "execution-opened": {
225
+ const e = d.execution;
226
+ check(
227
+ shape(e, ["id", "profile", "assignmentId", "recoveryId", "basisUserEntryId", "pair", "resultEntryIds"]) &&
228
+ identity(e.id) &&
229
+ ["solo", ...PROFILES].includes(e.profile) &&
230
+ nullableId(e.basisUserEntryId) &&
231
+ pair(e.pair) &&
232
+ uniqueStrings(e.resultEntryIds) &&
233
+ e.resultEntryIds.length === 0 &&
234
+ (e.assignmentId === undefined || identity(e.assignmentId)) &&
235
+ (e.recoveryId === undefined || identity(e.recoveryId)),
236
+ "invalid_execution",
237
+ );
238
+ break;
239
+ }
240
+ case "execution-bound":
241
+ check(
242
+ identity(d.executionId) &&
243
+ identity(d.assistantEntryId) &&
244
+ uniqueStrings(d.resultEntryIds) &&
245
+ ["completed", "failed", "aborted"].includes(d.outcome),
246
+ "invalid_binding",
247
+ );
248
+ break;
249
+ case "delegate-accepted": {
250
+ const u = d.unit,
251
+ a = d.assignment,
252
+ r = d.replacement;
253
+ check(
254
+ shape(u, ["id", "objective", "state", "assignmentIds"]) &&
255
+ identity(u.id) &&
256
+ text(u.objective) &&
257
+ u.state === "open" &&
258
+ uniqueStrings(u.assignmentIds),
259
+ "invalid_unit",
260
+ );
261
+ check(
262
+ shape(a, ["id", "unitId", "contract", "basisUserEntryId", "state", "delegateHandoffId"]) &&
263
+ identity(a.id) &&
264
+ identity(a.unitId) &&
265
+ text(a.contract) &&
266
+ nullableId(a.basisUserEntryId) &&
267
+ a.state === "outstanding" &&
268
+ identity(a.delegateHandoffId) &&
269
+ handoff(d.handoff),
270
+ "invalid_assignment",
271
+ );
272
+ check(
273
+ r === undefined ||
274
+ (shape(r, ["assignmentId", "supersededHandoffId", "reason"]) &&
275
+ identity(r.assignmentId) &&
276
+ text(r.reason, 2048) &&
277
+ (r.supersededHandoffId === undefined || identity(r.supersededHandoffId))),
278
+ "invalid_replacement",
279
+ );
280
+ break;
281
+ }
282
+ case "return-accepted":
283
+ check(handoff(d.handoff) && d.handoff.kind === "return", "invalid_return");
284
+ break;
285
+ case "recovery-request-accepted":
286
+ check(
287
+ recovery(d.recovery) &&
288
+ handoff(d.handoff) &&
289
+ d.handoff.kind === "recovery-request" &&
290
+ d.recovery.requestHandoffId === d.handoff.id &&
291
+ d.recovery.assignmentId === d.handoff.assignmentId &&
292
+ d.recovery.request === d.handoff.text,
293
+ "invalid_recovery_request",
294
+ );
295
+ break;
296
+ case "recovery-supplement-accepted":
297
+ check(
298
+ identity(d.recoveryId) && handoff(d.handoff) && d.handoff.kind === "recovery-return",
299
+ "invalid_recovery_supplement",
300
+ );
301
+ break;
302
+ case "recovery-cancelled":
303
+ check(identity(d.recoveryId) && text(d.reason, 2048), "invalid_recovery_cancellation");
304
+ break;
305
+ case "handoff-retry-requested":
306
+ check(
307
+ identity(d.attemptId) &&
308
+ identity(d.executionId) &&
309
+ identity(d.toolCallId) &&
310
+ Number.isSafeInteger(d.reportRevision) &&
311
+ d.reportRevision > 0 &&
312
+ Number.isSafeInteger(d.selectionRevision) &&
313
+ d.selectionRevision >= 0,
314
+ "invalid_retry",
315
+ );
316
+ break;
317
+ case "handoff-prepared":
318
+ check(reservation(d.reservation), "invalid_reservation");
319
+ break;
320
+ case "handoff-state":
321
+ check(
322
+ ["configured", "blocked", "superseded"].includes(d.state) &&
323
+ (d.reason === undefined || text(d.reason, 4096)) &&
324
+ (d.observedPair === undefined || pair(d.observedPair)),
325
+ "invalid_delivery",
326
+ );
327
+ break;
328
+ case "selection-changed": {
329
+ const s = d.selection;
330
+ check(
331
+ identity(d.assignmentId) &&
332
+ shape(s, ["revision", "selected", "unresolved", "withdrawals"]) &&
333
+ Number.isSafeInteger(s.revision) &&
334
+ s.revision > 0 &&
335
+ uniqueStrings(s.selected) &&
336
+ problemList(s.unresolved) &&
337
+ Array.isArray(s.withdrawals) &&
338
+ s.withdrawals.every((w) => shape(w, ["ref", "reason"]) && identity(w.ref) && text(w.reason, 2048)),
339
+ "invalid_selection",
340
+ );
341
+ break;
342
+ }
343
+ case "assessment-suspended":
344
+ check(
345
+ ["user-attention", "delivery-gap"].includes(d.reason) &&
346
+ nullableId(d.basisUserEntryId) &&
347
+ problemList(d.problems),
348
+ "invalid_suspension",
349
+ );
350
+ break;
351
+ case "assessment-resumed":
352
+ check(
353
+ nullableId(d.basisUserEntryId) && (d.reservation === undefined || reservation(d.reservation)),
354
+ "invalid_resumption",
355
+ );
356
+ break;
357
+ case "unit-closed":
358
+ check(
359
+ identity(d.unitId) &&
360
+ ["accepted", "cancelled", "deferred"].includes(d.outcome) &&
361
+ text(d.assessment) &&
362
+ (d.supersededAssignmentId === undefined || identity(d.supersededAssignmentId)),
363
+ "invalid_closure",
364
+ );
365
+ break;
366
+ case "request-observed":
367
+ check(
368
+ identity(d.executionId) &&
369
+ ["solo", ...PROFILES].includes(d.profile) &&
370
+ ["assembled", "payload-hook"].includes(d.boundary) &&
371
+ /^[a-f0-9]{64}$/.test(d.manifestHash) &&
372
+ (d.selectionRevision === undefined ||
373
+ (Number.isSafeInteger(d.selectionRevision) && d.selectionRevision >= 0)),
374
+ "invalid_request_observation",
375
+ );
376
+ break;
377
+ case "sources-exposed":
378
+ check(
379
+ identity(d.executionId) &&
380
+ ["solo", ...PROFILES].includes(d.view) &&
381
+ Array.isArray(d.sources) &&
382
+ d.sources.every((s) => shape(s, ["ref", "bodyHash"]) && identity(s.ref) && /^[a-f0-9]{64}$/.test(s.bodyHash)),
383
+ "invalid_exposure",
384
+ );
385
+ break;
386
+ }
387
+ return structuredClone(raw);
388
+ }
@@ -1,14 +1,15 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { activeReadOnlySessionBranch, readOnlySessionSnapshot, ReadOnlySessionError } from "./read-only-session.js";
3
2
  import {
4
- ROUTING_ENTRY,
5
- RoutingError,
6
- canonical,
7
- eventKey,
8
- eventValue,
9
- requireCondition as check,
10
- } from "../cognitive-routing-v2/types.js";
11
- import { parseRoutingEvent, reduce, replay } from "../cognitive-routing-v2/state.js";
3
+ activeReadOnlySessionBranch,
4
+ persistedTailMatches,
5
+ readOnlySessionSnapshot,
6
+ ReadOnlySessionError,
7
+ } from "../host/read-only-session.js";
8
+ import { ROUTING_ENTRY, RoutingError, canonical, eventKey, eventValue, requireCondition as check } from "./types.js";
9
+ import { parseRoutingEvent } from "./event-schema.js";
10
+ import { reduce, replay } from "./state.js";
11
+ import { sessionStage, stagedFor } from "../host/staging.js";
12
+ import { cachedBranch } from "../host/branch.js";
12
13
  export class EventStore {
13
14
  pi;
14
15
  reader;
@@ -25,11 +26,19 @@ export class EventStore {
25
26
  get blocked() {
26
27
  return this.fault ?? (!this.ready ? "acknowledgment_unclassified" : undefined);
27
28
  }
29
+ /** Routing state from the branch plus control changes staged since the last prompt. */
28
30
  state() {
29
- const entries = this.reader.getBranch();
31
+ const staged = stagedFor(this.reader)?.events ?? [];
32
+ return staged.reduce((state, event) => reduce(state, event), this.branchState());
33
+ }
34
+ branchState() {
35
+ const entries = cachedBranch(this.reader);
36
+ // The cached branch is the same frozen array until the leaf moves, so an unchanged branch needs no comparison.
37
+ if (entries === this.cachedEntries) return this.cachedState;
30
38
  const prefix =
31
39
  this.cachedEntries.length <= entries.length && this.cachedEntries.every((entry, i) => entry === entries[i]);
32
- if (!prefix) {
40
+ // An empty cache is a trivial prefix; the copying reducer would make that first fold quadratic.
41
+ if (!prefix || this.cachedEntries.length === 0) {
33
42
  this.cachedState = replay(entries);
34
43
  } else {
35
44
  let state = this.cachedState;
@@ -38,7 +47,7 @@ export class EventStore {
38
47
  state = reduce(state, parseRoutingEvent(entry.data));
39
48
  this.cachedState = state;
40
49
  }
41
- this.cachedEntries = [...entries];
50
+ this.cachedEntries = entries;
42
51
  return this.cachedState;
43
52
  }
44
53
  block(reason) {
@@ -58,7 +67,7 @@ export class EventStore {
58
67
  async reconcile() {
59
68
  const acknowledged = this.ready && !this.fault;
60
69
  this.ready = false;
61
- const branch = this.reader.getBranch();
70
+ const branch = cachedBranch(this.reader);
62
71
  const live = branch.filter((e) => e.type === "custom" && e.customType === ROUTING_ENTRY);
63
72
  try {
64
73
  const ids = new Set();
@@ -115,6 +124,20 @@ export class EventStore {
115
124
  }
116
125
  const path = this.reader.getSessionFile();
117
126
  check(path, "persisted_snapshot_unavailable", "Existing or uncertain routing state needs a persisted snapshot.");
127
+ // Pi parsed this file when it opened the session and appends each entry synchronously after that. When the
128
+ // bytes appended since that trust point are exactly Pi's in-memory entries, the live branch is persisted and
129
+ // no snapshot is needed. Otherwise the full comparison below decides.
130
+ if (await persistedTailMatches(this.reader)) {
131
+ const known = new Map();
132
+ for (const entry of live) {
133
+ const event = parseRoutingEvent(entry.data);
134
+ known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
135
+ }
136
+ this.acknowledge(known);
137
+ this.cachedEntries = branch;
138
+ this.cachedState = replay(branch);
139
+ return;
140
+ }
118
141
  const snapshot = await readOnlySessionSnapshot(path);
119
142
  check(snapshot.sessionId === this.reader.getSessionId(), "session_identity_changed");
120
143
  const persistedBranch = activeReadOnlySessionBranch(snapshot, this.reader.getLeafId());
@@ -123,14 +146,16 @@ export class EventStore {
123
146
  "snapshot_ancestry_mismatch",
124
147
  "Live branch is not the complete persisted ancestry for its claimed leaf.",
125
148
  );
126
- replay(persistedBranch);
149
+ const replayed = replay(persistedBranch);
127
150
  const disk = new Map(persistedBranch.map((e) => [e.id, e]));
128
151
  const known = new Map();
129
152
  // Compare the live selected ancestry, not whichever branch was last written on disk.
130
153
  for (const entry of branch) {
131
154
  const captured = disk.get(entry.id);
155
+ // Both copies come from the same JSON, so their keys share an order and the native serializer decides
156
+ // equality at a fraction of canonical()'s cost; canonical() settles only a mismatch in key order.
132
157
  check(
133
- captured && canonical(captured) === canonical(entry),
158
+ captured && (JSON.stringify(captured) === JSON.stringify(entry) || canonical(captured) === canonical(entry)),
134
159
  "snapshot_divergence",
135
160
  "Live ancestry is not fully established by the fresh persisted snapshot; reconcile the session before retrying.",
136
161
  );
@@ -139,18 +164,10 @@ export class EventStore {
139
164
  known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
140
165
  }
141
166
  }
142
- for (const [key, event] of this.attempted)
143
- check(
144
- known.get(key)?.value === eventValue(event) && known.get(key)?.eventId === event.eventId,
145
- "append_acknowledgment_uncertain",
146
- "The attempted routing event is not established by persisted readback.",
147
- );
148
- this.observed = known;
149
- this.attempted.clear();
150
- this.fault = undefined;
151
- this.ready = true;
152
- this.cachedEntries = [];
153
- this.cachedState = replay([]);
167
+ this.acknowledge(known);
168
+ // The live branch matches the persisted one entry for entry, so its replay is the state the next read needs.
169
+ this.cachedEntries = branch;
170
+ this.cachedState = replayed;
154
171
  } catch (error) {
155
172
  this.block(
156
173
  error instanceof RoutingError
@@ -162,20 +179,44 @@ export class EventStore {
162
179
  throw error;
163
180
  }
164
181
  }
165
- append(event) {
182
+ /** Accept persisted events as observed; every attempted (uncertain) append must be among them. */
183
+ acknowledge(known) {
184
+ for (const [key, event] of this.attempted)
185
+ check(
186
+ known.get(key)?.value === eventValue(event) && known.get(key)?.eventId === event.eventId,
187
+ "append_acknowledgment_uncertain",
188
+ "The attempted routing event is not established by persisted readback.",
189
+ );
190
+ this.observed = known;
191
+ this.attempted.clear();
192
+ this.fault = undefined;
193
+ this.ready = true;
194
+ }
195
+ /** Append an event; with `stage`, hold it for the next prompt instead of writing it to the session now. */
196
+ append(event, stage = false) {
166
197
  const value = parseRoutingEvent(event),
167
198
  key = eventKey(value),
168
199
  body = eventValue(value);
169
200
  if (this.blocked) throw new RoutingError("acknowledgment_uncertain", this.blocked);
201
+ const staged = stage ? sessionStage(this.reader) : stagedFor(this.reader);
170
202
  const prior = this.state().events.get(key);
203
+ if (prior && staged?.events.some((event) => eventKey(event) === key)) {
204
+ check(eventValue(prior) === body, "operation_conflict");
205
+ return prior;
206
+ }
207
+ if (stage && staged) {
208
+ reduce(this.state(), value);
209
+ staged.events.push(value);
210
+ return value;
211
+ }
212
+ // Replay applies events in session order, so anything staged is written before a later event.
213
+ if (staged?.events.length) this.writeStaged(staged.events.splice(0));
171
214
  if (prior) {
172
215
  check(eventValue(prior) === body, "operation_conflict");
173
- const occurrence = this.reader
174
- .getBranch()
175
- .find(
176
- (entry) =>
177
- entry.type === "custom" && entry.customType === ROUTING_ENTRY && entry.data?.eventId === prior.eventId,
178
- );
216
+ const occurrence = cachedBranch(this.reader).find(
217
+ (entry) =>
218
+ entry.type === "custom" && entry.customType === ROUTING_ENTRY && entry.data?.eventId === prior.eventId,
219
+ );
179
220
  if (
180
221
  this.observed.get(key)?.value !== body ||
181
222
  this.observed.get(key)?.eventId !== prior.eventId ||
@@ -187,23 +228,23 @@ export class EventStore {
187
228
  return prior;
188
229
  }
189
230
  // Validate the whole state transition before mutating Pi's session memory.
190
- const candidate = reduce(this.state(), value);
231
+ const candidate = reduce(this.branchState(), value);
191
232
  const baseline = this.cachedEntries;
192
233
  this.attempted.set(key, value);
193
234
  try {
194
235
  this.pi.appendEntry(ROUTING_ENTRY, value);
195
- const found = this.reader
196
- .getBranch()
197
- .find((e) => e.type === "custom" && e.customType === ROUTING_ENTRY && e.data?.eventId === value.eventId);
236
+ const found = cachedBranch(this.reader).find(
237
+ (e) => e.type === "custom" && e.customType === ROUTING_ENTRY && e.data?.eventId === value.eventId,
238
+ );
198
239
  check(found && eventValue(parseRoutingEvent(found.data)) === body, "append_not_observed");
199
240
  this.observed.set(key, { value: body, eventId: value.eventId, entryId: found.id });
200
241
  this.attempted.delete(key);
201
- const branch = this.reader.getBranch();
242
+ const branch = cachedBranch(this.reader);
202
243
  const delta = branch.slice(baseline.length).filter((e) => e.type === "custom" && e.customType === ROUTING_ENTRY);
203
244
  if (baseline.every((e, i) => branch[i] === e) && delta.length === 1 && delta[0].id === found.id) {
204
245
  // Publish the already validated candidate only after acknowledged native append.
205
246
  this.cachedState = candidate;
206
- this.cachedEntries = [...branch];
247
+ this.cachedEntries = branch;
207
248
  }
208
249
  return value;
209
250
  } catch (error) {
@@ -214,4 +255,26 @@ export class EventStore {
214
255
  );
215
256
  }
216
257
  }
258
+ /**
259
+ * Write the net effect of control changes staged since the last prompt: at most one delegation, one profile
260
+ * override, and one control event, each only when it changes the recorded state.
261
+ */
262
+ writeStaged(events) {
263
+ if (!events.length) return;
264
+ const staged = events;
265
+ const before = this.branchState();
266
+ const after = staged.reduce((state, event) => reduce(state, event), before);
267
+ const last = (type) => staged.filter((event) => event.data.type === type).at(-1);
268
+ if (before.delegationOverride !== after.delegationOverride) this.append(last("delegation-override"));
269
+ const changed = Object.fromEntries(
270
+ [...new Set([...before.profileOverrides.keys(), ...after.profileOverrides.keys()])]
271
+ .filter(
272
+ (profile) =>
273
+ canonical(before.profileOverrides.get(profile)) !== canonical(after.profileOverrides.get(profile)),
274
+ )
275
+ .map((profile) => [profile, after.profileOverrides.get(profile) ?? null]),
276
+ );
277
+ if (Object.keys(changed).length) this.append(this.make({ ...last("profile-overrides").data, overrides: changed }));
278
+ if (before.control !== after.control || before.profile !== after.profile) this.append(last("control"));
279
+ }
217
280
  }