@origintrail-official/dkg-agent 10.0.19 → 10.0.21

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 (733) hide show
  1. package/dist/approved-private-replica.d.ts +54 -0
  2. package/dist/approved-private-replica.d.ts.map +1 -0
  3. package/dist/approved-private-replica.js +148 -0
  4. package/dist/approved-private-replica.js.map +1 -0
  5. package/dist/bounded-operation.d.ts +2 -0
  6. package/dist/bounded-operation.d.ts.map +1 -1
  7. package/dist/bounded-operation.js +5 -0
  8. package/dist/bounded-operation.js.map +1 -1
  9. package/dist/chain-finality-confirmations-v1.d.ts +7 -0
  10. package/dist/chain-finality-confirmations-v1.d.ts.map +1 -0
  11. package/dist/chain-finality-confirmations-v1.js +9 -0
  12. package/dist/chain-finality-confirmations-v1.js.map +1 -0
  13. package/dist/chain-reconciler.d.ts +23 -0
  14. package/dist/chain-reconciler.d.ts.map +1 -1
  15. package/dist/chain-reconciler.js +35 -0
  16. package/dist/chain-reconciler.js.map +1 -1
  17. package/dist/confirmed-graph-scoped-vm-resolver.d.ts +7 -1
  18. package/dist/confirmed-graph-scoped-vm-resolver.d.ts.map +1 -1
  19. package/dist/confirmed-graph-scoped-vm-resolver.js +5 -3
  20. package/dist/confirmed-graph-scoped-vm-resolver.js.map +1 -1
  21. package/dist/context-graph-authority-dependency.d.ts +11 -0
  22. package/dist/context-graph-authority-dependency.d.ts.map +1 -0
  23. package/dist/context-graph-authority-dependency.js +50 -0
  24. package/dist/context-graph-authority-dependency.js.map +1 -0
  25. package/dist/context-graph-binding-state.d.ts +6 -0
  26. package/dist/context-graph-binding-state.d.ts.map +1 -1
  27. package/dist/context-graph-binding-state.js +17 -10
  28. package/dist/context-graph-binding-state.js.map +1 -1
  29. package/dist/context-graph-claim-proof.d.ts +74 -0
  30. package/dist/context-graph-claim-proof.d.ts.map +1 -0
  31. package/dist/context-graph-claim-proof.js +137 -0
  32. package/dist/context-graph-claim-proof.js.map +1 -0
  33. package/dist/context-graph-member-proof.d.ts +33 -0
  34. package/dist/context-graph-member-proof.d.ts.map +1 -0
  35. package/dist/context-graph-member-proof.js +142 -0
  36. package/dist/context-graph-member-proof.js.map +1 -0
  37. package/dist/context-graph-membership-persist-scheduler.d.ts +7 -23
  38. package/dist/context-graph-membership-persist-scheduler.d.ts.map +1 -1
  39. package/dist/context-graph-membership-persist-scheduler.js +12 -88
  40. package/dist/context-graph-membership-persist-scheduler.js.map +1 -1
  41. package/dist/context-graph-meta-confirmation.d.ts +22 -2
  42. package/dist/context-graph-meta-confirmation.d.ts.map +1 -1
  43. package/dist/context-graph-meta-confirmation.js +54 -23
  44. package/dist/context-graph-meta-confirmation.js.map +1 -1
  45. package/dist/context-graph-meta-projection.d.ts +13 -2
  46. package/dist/context-graph-meta-projection.d.ts.map +1 -1
  47. package/dist/context-graph-meta-projection.js +61 -24
  48. package/dist/context-graph-meta-projection.js.map +1 -1
  49. package/dist/context-graph-metadata-relocation.d.ts +18 -30
  50. package/dist/context-graph-metadata-relocation.d.ts.map +1 -1
  51. package/dist/context-graph-metadata-relocation.js +32 -2
  52. package/dist/context-graph-metadata-relocation.js.map +1 -1
  53. package/dist/context-graph-private-meta-proof.d.ts +8 -9
  54. package/dist/context-graph-private-meta-proof.d.ts.map +1 -1
  55. package/dist/context-graph-private-meta-proof.js +25 -89
  56. package/dist/context-graph-private-meta-proof.js.map +1 -1
  57. package/dist/context-graph-public-meta-proof.d.ts +11 -1
  58. package/dist/context-graph-public-meta-proof.d.ts.map +1 -1
  59. package/dist/context-graph-public-meta-proof.js +23 -3
  60. package/dist/context-graph-public-meta-proof.js.map +1 -1
  61. package/dist/context-graph-read-authority.d.ts +30 -3
  62. package/dist/context-graph-read-authority.d.ts.map +1 -1
  63. package/dist/context-graph-read-authority.js +62 -18
  64. package/dist/context-graph-read-authority.js.map +1 -1
  65. package/dist/context-graph-storage-discovery.d.ts +14 -4
  66. package/dist/context-graph-storage-discovery.d.ts.map +1 -1
  67. package/dist/context-graph-storage-discovery.js +27 -7
  68. package/dist/context-graph-storage-discovery.js.map +1 -1
  69. package/dist/context-graph-subscription-authority-recovery.d.ts +7 -0
  70. package/dist/context-graph-subscription-authority-recovery.d.ts.map +1 -1
  71. package/dist/context-graph-subscription-authority-recovery.js +1 -0
  72. package/dist/context-graph-subscription-authority-recovery.js.map +1 -1
  73. package/dist/context-graph-subscription-persist-scheduler.d.ts +52 -0
  74. package/dist/context-graph-subscription-persist-scheduler.d.ts.map +1 -0
  75. package/dist/context-graph-subscription-persist-scheduler.js +78 -0
  76. package/dist/context-graph-subscription-persist-scheduler.js.map +1 -0
  77. package/dist/curator-dial-address.d.ts +16 -0
  78. package/dist/curator-dial-address.d.ts.map +1 -0
  79. package/dist/curator-dial-address.js +58 -0
  80. package/dist/curator-dial-address.js.map +1 -0
  81. package/dist/curator-meta-refresh.d.ts +18 -4
  82. package/dist/curator-meta-refresh.d.ts.map +1 -1
  83. package/dist/curator-meta-refresh.js +61 -58
  84. package/dist/curator-meta-refresh.js.map +1 -1
  85. package/dist/curator-peer-connection.d.ts +21 -0
  86. package/dist/curator-peer-connection.d.ts.map +1 -0
  87. package/dist/curator-peer-connection.js +41 -0
  88. package/dist/curator-peer-connection.js.map +1 -0
  89. package/dist/dkg-agent-agents-phonebook.d.ts +41 -0
  90. package/dist/dkg-agent-agents-phonebook.d.ts.map +1 -0
  91. package/dist/dkg-agent-agents-phonebook.js +155 -0
  92. package/dist/dkg-agent-agents-phonebook.js.map +1 -0
  93. package/dist/dkg-agent-base.d.ts +162 -24
  94. package/dist/dkg-agent-base.d.ts.map +1 -1
  95. package/dist/dkg-agent-base.js +175 -147
  96. package/dist/dkg-agent-base.js.map +1 -1
  97. package/dist/dkg-agent-cg-chain-observation.d.ts +6 -1
  98. package/dist/dkg-agent-cg-chain-observation.d.ts.map +1 -1
  99. package/dist/dkg-agent-cg-chain-observation.js +20 -5
  100. package/dist/dkg-agent-cg-chain-observation.js.map +1 -1
  101. package/dist/dkg-agent-cg-name-resolution.d.ts +38 -2
  102. package/dist/dkg-agent-cg-name-resolution.d.ts.map +1 -1
  103. package/dist/dkg-agent-cg-name-resolution.js +100 -8
  104. package/dist/dkg-agent-cg-name-resolution.js.map +1 -1
  105. package/dist/dkg-agent-cg-registry.d.ts +43 -3
  106. package/dist/dkg-agent-cg-registry.d.ts.map +1 -1
  107. package/dist/dkg-agent-cg-registry.js +267 -129
  108. package/dist/dkg-agent-cg-registry.js.map +1 -1
  109. package/dist/dkg-agent-cg-resolve.d.ts +6 -4
  110. package/dist/dkg-agent-cg-resolve.d.ts.map +1 -1
  111. package/dist/dkg-agent-cg-resolve.js +40 -18
  112. package/dist/dkg-agent-cg-resolve.js.map +1 -1
  113. package/dist/dkg-agent-constants.d.ts +10 -2
  114. package/dist/dkg-agent-constants.d.ts.map +1 -1
  115. package/dist/dkg-agent-constants.js +10 -2
  116. package/dist/dkg-agent-constants.js.map +1 -1
  117. package/dist/dkg-agent-context-graph.d.ts.map +1 -1
  118. package/dist/dkg-agent-context-graph.js +1 -5
  119. package/dist/dkg-agent-context-graph.js.map +1 -1
  120. package/dist/dkg-agent-crypto.d.ts +128 -21
  121. package/dist/dkg-agent-crypto.d.ts.map +1 -1
  122. package/dist/dkg-agent-crypto.js +824 -176
  123. package/dist/dkg-agent-crypto.js.map +1 -1
  124. package/dist/dkg-agent-diagnostics.d.ts.map +1 -1
  125. package/dist/dkg-agent-diagnostics.js +4 -3
  126. package/dist/dkg-agent-diagnostics.js.map +1 -1
  127. package/dist/dkg-agent-helpers.d.ts +2 -2
  128. package/dist/dkg-agent-helpers.d.ts.map +1 -1
  129. package/dist/dkg-agent-helpers.js +3 -3
  130. package/dist/dkg-agent-helpers.js.map +1 -1
  131. package/dist/dkg-agent-join.d.ts +12 -1
  132. package/dist/dkg-agent-join.d.ts.map +1 -1
  133. package/dist/dkg-agent-join.js +80 -92
  134. package/dist/dkg-agent-join.js.map +1 -1
  135. package/dist/dkg-agent-lifecycle.d.ts +129 -22
  136. package/dist/dkg-agent-lifecycle.d.ts.map +1 -1
  137. package/dist/dkg-agent-lifecycle.js +690 -211
  138. package/dist/dkg-agent-lifecycle.js.map +1 -1
  139. package/dist/dkg-agent-publish.d.ts +33 -40
  140. package/dist/dkg-agent-publish.d.ts.map +1 -1
  141. package/dist/dkg-agent-publish.js +194 -281
  142. package/dist/dkg-agent-publish.js.map +1 -1
  143. package/dist/dkg-agent-query.d.ts +7 -0
  144. package/dist/dkg-agent-query.d.ts.map +1 -1
  145. package/dist/dkg-agent-query.js +102 -86
  146. package/dist/dkg-agent-query.js.map +1 -1
  147. package/dist/dkg-agent-registered-private-empty-vm.d.ts +70 -0
  148. package/dist/dkg-agent-registered-private-empty-vm.d.ts.map +1 -0
  149. package/dist/dkg-agent-registered-private-empty-vm.js +133 -0
  150. package/dist/dkg-agent-registered-private-empty-vm.js.map +1 -0
  151. package/dist/dkg-agent-rfc64-catalog-auto-publish.d.ts +8 -3
  152. package/dist/dkg-agent-rfc64-catalog-auto-publish.d.ts.map +1 -1
  153. package/dist/dkg-agent-rfc64-catalog-auto-publish.js +45 -51
  154. package/dist/dkg-agent-rfc64-catalog-auto-publish.js.map +1 -1
  155. package/dist/dkg-agent-rfc64-catalog.d.ts +87 -80
  156. package/dist/dkg-agent-rfc64-catalog.d.ts.map +1 -1
  157. package/dist/dkg-agent-rfc64-catalog.js +726 -476
  158. package/dist/dkg-agent-rfc64-catalog.js.map +1 -1
  159. package/dist/dkg-agent-rfc64-meta-bootstrap.d.ts +6 -1
  160. package/dist/dkg-agent-rfc64-meta-bootstrap.d.ts.map +1 -1
  161. package/dist/dkg-agent-rfc64-meta-bootstrap.js +29 -13
  162. package/dist/dkg-agent-rfc64-meta-bootstrap.js.map +1 -1
  163. package/dist/dkg-agent-rfc64-swm-catalog-projection-supervisor.d.ts +8 -0
  164. package/dist/dkg-agent-rfc64-swm-catalog-projection-supervisor.d.ts.map +1 -1
  165. package/dist/dkg-agent-rfc64-swm-catalog-projection-supervisor.js +249 -40
  166. package/dist/dkg-agent-rfc64-swm-catalog-projection-supervisor.js.map +1 -1
  167. package/dist/dkg-agent-rfc64-swm-catalog-projection.d.ts.map +1 -1
  168. package/dist/dkg-agent-rfc64-swm-catalog-projection.js +3 -2
  169. package/dist/dkg-agent-rfc64-swm-catalog-projection.js.map +1 -1
  170. package/dist/dkg-agent-swm-host.d.ts +217 -52
  171. package/dist/dkg-agent-swm-host.d.ts.map +1 -1
  172. package/dist/dkg-agent-swm-host.js +1305 -858
  173. package/dist/dkg-agent-swm-host.js.map +1 -1
  174. package/dist/dkg-agent-swm-state.d.ts +15 -0
  175. package/dist/dkg-agent-swm-state.d.ts.map +1 -1
  176. package/dist/dkg-agent-swm-state.js +43 -0
  177. package/dist/dkg-agent-swm-state.js.map +1 -1
  178. package/dist/dkg-agent-swm-substrate.d.ts +49 -8
  179. package/dist/dkg-agent-swm-substrate.d.ts.map +1 -1
  180. package/dist/dkg-agent-swm-substrate.js +219 -46
  181. package/dist/dkg-agent-swm-substrate.js.map +1 -1
  182. package/dist/dkg-agent-types.d.ts +86 -27
  183. package/dist/dkg-agent-types.d.ts.map +1 -1
  184. package/dist/dkg-agent-types.js.map +1 -1
  185. package/dist/dkg-agent-vm-promotion.d.ts +4 -0
  186. package/dist/dkg-agent-vm-promotion.d.ts.map +1 -1
  187. package/dist/dkg-agent-vm-promotion.js +40 -5
  188. package/dist/dkg-agent-vm-promotion.js.map +1 -1
  189. package/dist/dkg-agent-vm-reconcile-scheduling.d.ts +7 -0
  190. package/dist/dkg-agent-vm-reconcile-scheduling.d.ts.map +1 -1
  191. package/dist/dkg-agent-vm-reconcile-scheduling.js +18 -0
  192. package/dist/dkg-agent-vm-reconcile-scheduling.js.map +1 -1
  193. package/dist/dkg-agent.d.ts +31 -2
  194. package/dist/dkg-agent.d.ts.map +1 -1
  195. package/dist/dkg-agent.js +188 -72
  196. package/dist/dkg-agent.js.map +1 -1
  197. package/dist/exact-graph-content-verifier.d.ts.map +1 -1
  198. package/dist/exact-graph-content-verifier.js +3 -2
  199. package/dist/exact-graph-content-verifier.js.map +1 -1
  200. package/dist/finalization-handler.d.ts +26 -211
  201. package/dist/finalization-handler.d.ts.map +1 -1
  202. package/dist/finalization-handler.js +94 -1652
  203. package/dist/finalization-handler.js.map +1 -1
  204. package/dist/finalization-recovery-sqlite-displacement.d.ts +37 -0
  205. package/dist/finalization-recovery-sqlite-displacement.d.ts.map +1 -0
  206. package/dist/finalization-recovery-sqlite-displacement.js +102 -0
  207. package/dist/finalization-recovery-sqlite-displacement.js.map +1 -0
  208. package/dist/finalization-recovery-sqlite-policy.d.ts +20 -4
  209. package/dist/finalization-recovery-sqlite-policy.d.ts.map +1 -1
  210. package/dist/finalization-recovery-sqlite-policy.js +21 -5
  211. package/dist/finalization-recovery-sqlite-policy.js.map +1 -1
  212. package/dist/finalization-recovery-sqlite-rows.d.ts +53 -0
  213. package/dist/finalization-recovery-sqlite-rows.d.ts.map +1 -0
  214. package/dist/finalization-recovery-sqlite-rows.js +77 -0
  215. package/dist/finalization-recovery-sqlite-rows.js.map +1 -0
  216. package/dist/finalization-recovery-sqlite-store.d.ts +10 -4
  217. package/dist/finalization-recovery-sqlite-store.d.ts.map +1 -1
  218. package/dist/finalization-recovery-sqlite-store.js +49 -50
  219. package/dist/finalization-recovery-sqlite-store.js.map +1 -1
  220. package/dist/finalization-recovery-store.d.ts +18 -0
  221. package/dist/finalization-recovery-store.d.ts.map +1 -1
  222. package/dist/finalization-recovery-store.js +24 -0
  223. package/dist/finalization-recovery-store.js.map +1 -1
  224. package/dist/finalization-recovery-worker.js +1 -1
  225. package/dist/finalization-recovery-worker.js.map +1 -1
  226. package/dist/finalization-recovery.d.ts +2 -3
  227. package/dist/finalization-recovery.d.ts.map +1 -1
  228. package/dist/finalization-recovery.js +12 -9
  229. package/dist/finalization-recovery.js.map +1 -1
  230. package/dist/finalized-authority-cold-resolution.d.ts +6 -1
  231. package/dist/finalized-authority-cold-resolution.d.ts.map +1 -1
  232. package/dist/finalized-authority-cold-resolution.js +22 -3
  233. package/dist/finalized-authority-cold-resolution.js.map +1 -1
  234. package/dist/gossip-publish-handler.d.ts.map +1 -1
  235. package/dist/gossip-publish-handler.js +4 -2
  236. package/dist/gossip-publish-handler.js.map +1 -1
  237. package/dist/gossip-session.d.ts +29 -0
  238. package/dist/gossip-session.d.ts.map +1 -0
  239. package/dist/gossip-session.js +48 -0
  240. package/dist/gossip-session.js.map +1 -0
  241. package/dist/index.d.ts +4 -1
  242. package/dist/index.d.ts.map +1 -1
  243. package/dist/index.js +4 -0
  244. package/dist/index.js.map +1 -1
  245. package/dist/internal/context-graph-authority/approved-member-acceptance.d.ts +61 -0
  246. package/dist/internal/context-graph-authority/approved-member-acceptance.d.ts.map +1 -0
  247. package/dist/internal/context-graph-authority/approved-member-acceptance.js +75 -0
  248. package/dist/internal/context-graph-authority/approved-member-acceptance.js.map +1 -0
  249. package/dist/internal/context-graph-authority/context-graph-agent-gate-authority.d.ts +5 -5
  250. package/dist/internal/context-graph-authority/context-graph-agent-gate-authority.d.ts.map +1 -1
  251. package/dist/internal/context-graph-authority/context-graph-agent-gate-authority.js +59 -49
  252. package/dist/internal/context-graph-authority/context-graph-agent-gate-authority.js.map +1 -1
  253. package/dist/internal/context-graph-authority/context-graph-authority.d.ts +1 -0
  254. package/dist/internal/context-graph-authority/context-graph-authority.d.ts.map +1 -1
  255. package/dist/internal/context-graph-authority/context-graph-authority.js +25 -0
  256. package/dist/internal/context-graph-authority/context-graph-authority.js.map +1 -1
  257. package/dist/internal/context-graph-authority/finalized-target-projection.d.ts +17 -0
  258. package/dist/internal/context-graph-authority/finalized-target-projection.d.ts.map +1 -0
  259. package/dist/internal/context-graph-authority/finalized-target-projection.js +36 -0
  260. package/dist/internal/context-graph-authority/finalized-target-projection.js.map +1 -0
  261. package/dist/internal/context-graph-authority/swm-transport-authority.d.ts +84 -0
  262. package/dist/internal/context-graph-authority/swm-transport-authority.d.ts.map +1 -0
  263. package/dist/internal/context-graph-authority/swm-transport-authority.js +175 -0
  264. package/dist/internal/context-graph-authority/swm-transport-authority.js.map +1 -0
  265. package/dist/internal/context-graph-binding-abort.d.ts +3 -0
  266. package/dist/internal/context-graph-binding-abort.d.ts.map +1 -0
  267. package/dist/internal/context-graph-binding-abort.js +32 -0
  268. package/dist/internal/context-graph-binding-abort.js.map +1 -0
  269. package/dist/internal/context-graph-cache-invalidating-store.d.ts +3 -0
  270. package/dist/internal/context-graph-cache-invalidating-store.d.ts.map +1 -0
  271. package/dist/internal/context-graph-cache-invalidating-store.js +167 -0
  272. package/dist/internal/context-graph-cache-invalidating-store.js.map +1 -0
  273. package/dist/internal/context-graph-meta-record-copy.d.ts +3 -0
  274. package/dist/internal/context-graph-meta-record-copy.d.ts.map +1 -0
  275. package/dist/internal/context-graph-meta-record-copy.js +21 -0
  276. package/dist/internal/context-graph-meta-record-copy.js.map +1 -0
  277. package/dist/internal/gossip/host-mode-handler.d.ts +11 -0
  278. package/dist/internal/gossip/host-mode-handler.d.ts.map +1 -0
  279. package/dist/internal/gossip/host-mode-handler.js +41 -0
  280. package/dist/internal/gossip/host-mode-handler.js.map +1 -0
  281. package/dist/internal/join-encryption-key-bundle.d.ts +10 -0
  282. package/dist/internal/join-encryption-key-bundle.d.ts.map +1 -0
  283. package/dist/internal/join-encryption-key-bundle.js +72 -0
  284. package/dist/internal/join-encryption-key-bundle.js.map +1 -0
  285. package/dist/internal/knowledge-asset-vm-publish-request.d.ts +31 -0
  286. package/dist/internal/knowledge-asset-vm-publish-request.d.ts.map +1 -0
  287. package/dist/internal/knowledge-asset-vm-publish-request.js +91 -0
  288. package/dist/internal/knowledge-asset-vm-publish-request.js.map +1 -0
  289. package/dist/internal/lifecycle-sync-policy.d.ts +5 -0
  290. package/dist/internal/lifecycle-sync-policy.d.ts.map +1 -0
  291. package/dist/internal/lifecycle-sync-policy.js +12 -0
  292. package/dist/internal/lifecycle-sync-policy.js.map +1 -0
  293. package/dist/internal/local-private-member.d.ts +14 -0
  294. package/dist/internal/local-private-member.d.ts.map +1 -0
  295. package/dist/internal/local-private-member.js +21 -0
  296. package/dist/internal/local-private-member.js.map +1 -0
  297. package/dist/internal/promote/assertion-promote-precommit.d.ts +41 -1
  298. package/dist/internal/promote/assertion-promote-precommit.d.ts.map +1 -1
  299. package/dist/internal/promote/assertion-promote-precommit.js +79 -22
  300. package/dist/internal/promote/assertion-promote-precommit.js.map +1 -1
  301. package/dist/internal/storage-ack-owned-request.d.ts +16 -0
  302. package/dist/internal/storage-ack-owned-request.d.ts.map +1 -0
  303. package/dist/internal/storage-ack-owned-request.js +16 -0
  304. package/dist/internal/storage-ack-owned-request.js.map +1 -0
  305. package/dist/internal/vm-reconcile-local-admission-wait.d.ts +96 -0
  306. package/dist/internal/vm-reconcile-local-admission-wait.d.ts.map +1 -0
  307. package/dist/internal/vm-reconcile-local-admission-wait.js +226 -0
  308. package/dist/internal/vm-reconcile-local-admission-wait.js.map +1 -0
  309. package/dist/internal/vm-reconcile-read-authority.d.ts +27 -0
  310. package/dist/internal/vm-reconcile-read-authority.d.ts.map +1 -0
  311. package/dist/internal/vm-reconcile-read-authority.js +33 -0
  312. package/dist/internal/vm-reconcile-read-authority.js.map +1 -0
  313. package/dist/internal/vm-reconcile-sweep-admission.d.ts +2 -0
  314. package/dist/internal/vm-reconcile-sweep-admission.d.ts.map +1 -1
  315. package/dist/internal/vm-reconcile-sweep.d.ts +16 -4
  316. package/dist/internal/vm-reconcile-sweep.d.ts.map +1 -1
  317. package/dist/internal/vm-reconcile-sweep.js +91 -50
  318. package/dist/internal/vm-reconcile-sweep.js.map +1 -1
  319. package/dist/internal/workspace-projected-delegatees.d.ts +3 -0
  320. package/dist/internal/workspace-projected-delegatees.d.ts.map +1 -0
  321. package/dist/internal/workspace-projected-delegatees.js +34 -0
  322. package/dist/internal/workspace-projected-delegatees.js.map +1 -0
  323. package/dist/join-approval-restart-projection.d.ts +14 -0
  324. package/dist/join-approval-restart-projection.d.ts.map +1 -0
  325. package/dist/join-approval-restart-projection.js +32 -0
  326. package/dist/join-approval-restart-projection.js.map +1 -0
  327. package/dist/keyed-persist-scheduler.d.ts +72 -0
  328. package/dist/keyed-persist-scheduler.d.ts.map +1 -0
  329. package/dist/keyed-persist-scheduler.js +140 -0
  330. package/dist/keyed-persist-scheduler.js.map +1 -0
  331. package/dist/map-with-concurrency.d.ts +7 -0
  332. package/dist/map-with-concurrency.d.ts.map +1 -1
  333. package/dist/map-with-concurrency.js +36 -0
  334. package/dist/map-with-concurrency.js.map +1 -1
  335. package/dist/p2p/network-admission-coordinator.d.ts +15 -0
  336. package/dist/p2p/network-admission-coordinator.d.ts.map +1 -1
  337. package/dist/p2p/network-admission-coordinator.js +20 -2
  338. package/dist/p2p/network-admission-coordinator.js.map +1 -1
  339. package/dist/p2p/peer-connect.d.ts +10 -1
  340. package/dist/p2p/peer-connect.d.ts.map +1 -1
  341. package/dist/p2p/peer-connect.js +11 -2
  342. package/dist/p2p/peer-connect.js.map +1 -1
  343. package/dist/p2p/peer-selection.d.ts +8 -0
  344. package/dist/p2p/peer-selection.d.ts.map +1 -1
  345. package/dist/p2p/peer-selection.js +21 -12
  346. package/dist/p2p/peer-selection.js.map +1 -1
  347. package/dist/p2p/protocol-readiness.d.ts +3 -3
  348. package/dist/p2p/protocol-readiness.d.ts.map +1 -1
  349. package/dist/p2p/protocol-readiness.js +4 -3
  350. package/dist/p2p/protocol-readiness.js.map +1 -1
  351. package/dist/prepare-unscoped-context-graph-read-checks.d.ts.map +1 -1
  352. package/dist/prepare-unscoped-context-graph-read-checks.js +12 -0
  353. package/dist/prepare-unscoped-context-graph-read-checks.js.map +1 -1
  354. package/dist/registered-context-graph-authority.d.ts +19 -0
  355. package/dist/registered-context-graph-authority.d.ts.map +1 -1
  356. package/dist/registered-private-empty-vm-attempt-v1.d.ts +34 -0
  357. package/dist/registered-private-empty-vm-attempt-v1.d.ts.map +1 -0
  358. package/dist/registered-private-empty-vm-attempt-v1.js +120 -0
  359. package/dist/registered-private-empty-vm-attempt-v1.js.map +1 -0
  360. package/dist/reserved-ka-id-allocation.d.ts +12 -0
  361. package/dist/reserved-ka-id-allocation.d.ts.map +1 -0
  362. package/dist/reserved-ka-id-allocation.js +25 -0
  363. package/dist/reserved-ka-id-allocation.js.map +1 -0
  364. package/dist/rfc64/applied-catalog-authority-transition-v1.d.ts.map +1 -1
  365. package/dist/rfc64/applied-catalog-authority-transition-v1.js +15 -17
  366. package/dist/rfc64/applied-catalog-authority-transition-v1.js.map +1 -1
  367. package/dist/rfc64/authority-rpc-circuit-breaker-v1.d.ts +5 -3
  368. package/dist/rfc64/authority-rpc-circuit-breaker-v1.d.ts.map +1 -1
  369. package/dist/rfc64/authority-rpc-circuit-breaker-v1.js +9 -0
  370. package/dist/rfc64/authority-rpc-circuit-breaker-v1.js.map +1 -1
  371. package/dist/rfc64/catalog-completion-evidence-v1.d.ts +38 -0
  372. package/dist/rfc64/catalog-completion-evidence-v1.d.ts.map +1 -0
  373. package/dist/rfc64/catalog-completion-evidence-v1.js +49 -0
  374. package/dist/rfc64/catalog-completion-evidence-v1.js.map +1 -0
  375. package/dist/rfc64/catalog-operational-applied-heads-v1.d.ts +12 -13
  376. package/dist/rfc64/catalog-operational-applied-heads-v1.d.ts.map +1 -1
  377. package/dist/rfc64/catalog-operational-applied-heads-v1.js +17 -52
  378. package/dist/rfc64/catalog-operational-applied-heads-v1.js.map +1 -1
  379. package/dist/rfc64/catalog-operational-reads-v1.d.ts +13 -0
  380. package/dist/rfc64/catalog-operational-reads-v1.d.ts.map +1 -0
  381. package/dist/rfc64/catalog-operational-reads-v1.js +56 -0
  382. package/dist/rfc64/catalog-operational-reads-v1.js.map +1 -0
  383. package/dist/rfc64/catalog-operational-targets-v1.d.ts +55 -0
  384. package/dist/rfc64/catalog-operational-targets-v1.d.ts.map +1 -0
  385. package/dist/rfc64/catalog-operational-targets-v1.js +379 -0
  386. package/dist/rfc64/catalog-operational-targets-v1.js.map +1 -0
  387. package/dist/rfc64/catalog-repair-diagnostics-v1.d.ts +28 -0
  388. package/dist/rfc64/catalog-repair-diagnostics-v1.d.ts.map +1 -0
  389. package/dist/rfc64/catalog-repair-diagnostics-v1.js +93 -0
  390. package/dist/rfc64/catalog-repair-diagnostics-v1.js.map +1 -0
  391. package/dist/rfc64/catalog-repair-retry-v1.d.ts +13 -0
  392. package/dist/rfc64/catalog-repair-retry-v1.d.ts.map +1 -0
  393. package/dist/rfc64/catalog-repair-retry-v1.js +39 -0
  394. package/dist/rfc64/catalog-repair-retry-v1.js.map +1 -0
  395. package/dist/rfc64/catalog-replay-recovery-runtime-v1.d.ts +52 -0
  396. package/dist/rfc64/catalog-replay-recovery-runtime-v1.d.ts.map +1 -1
  397. package/dist/rfc64/catalog-replay-recovery-runtime-v1.js +188 -32
  398. package/dist/rfc64/catalog-replay-recovery-runtime-v1.js.map +1 -1
  399. package/dist/rfc64/catalog-replay-snapshot-runtime-v1.d.ts +2 -8
  400. package/dist/rfc64/catalog-replay-snapshot-runtime-v1.d.ts.map +1 -1
  401. package/dist/rfc64/catalog-replay-snapshot-runtime-v1.js +10 -37
  402. package/dist/rfc64/catalog-replay-snapshot-runtime-v1.js.map +1 -1
  403. package/dist/rfc64/catalog-rollout-authority-v1.d.ts +13 -0
  404. package/dist/rfc64/catalog-rollout-authority-v1.d.ts.map +1 -1
  405. package/dist/rfc64/catalog-rollout-authority-v1.js +11 -0
  406. package/dist/rfc64/catalog-rollout-authority-v1.js.map +1 -1
  407. package/dist/rfc64/catalog-row-authorship.d.ts +12 -0
  408. package/dist/rfc64/catalog-row-authorship.d.ts.map +1 -1
  409. package/dist/rfc64/catalog-row-authorship.js +110 -57
  410. package/dist/rfc64/catalog-row-authorship.js.map +1 -1
  411. package/dist/rfc64/inventory-v1/candidate.d.ts +31 -10
  412. package/dist/rfc64/inventory-v1/candidate.d.ts.map +1 -1
  413. package/dist/rfc64/inventory-v1/candidate.js +51 -16
  414. package/dist/rfc64/inventory-v1/candidate.js.map +1 -1
  415. package/dist/rfc64/inventory-v1/index.d.ts +1 -1
  416. package/dist/rfc64/inventory-v1/index.d.ts.map +1 -1
  417. package/dist/rfc64/inventory-v1/index.js +1 -1
  418. package/dist/rfc64/inventory-v1/index.js.map +1 -1
  419. package/dist/rfc64/inventory-v1/open.d.ts.map +1 -1
  420. package/dist/rfc64/inventory-v1/open.js +7 -4
  421. package/dist/rfc64/inventory-v1/open.js.map +1 -1
  422. package/dist/rfc64/inventory-v1/swm-author-inventory-persistence.d.ts +3 -1
  423. package/dist/rfc64/inventory-v1/swm-author-inventory-persistence.d.ts.map +1 -1
  424. package/dist/rfc64/inventory-v1/swm-author-inventory-persistence.js +15 -0
  425. package/dist/rfc64/inventory-v1/swm-author-inventory-persistence.js.map +1 -1
  426. package/dist/rfc64/legacy-swm-boundary-codec-v1.d.ts +25 -0
  427. package/dist/rfc64/legacy-swm-boundary-codec-v1.d.ts.map +1 -0
  428. package/dist/rfc64/legacy-swm-boundary-codec-v1.js +127 -0
  429. package/dist/rfc64/legacy-swm-boundary-codec-v1.js.map +1 -0
  430. package/dist/rfc64/legacy-swm-boundary-v1.d.ts +12 -0
  431. package/dist/rfc64/legacy-swm-boundary-v1.d.ts.map +1 -1
  432. package/dist/rfc64/legacy-swm-boundary-v1.js +83 -144
  433. package/dist/rfc64/legacy-swm-boundary-v1.js.map +1 -1
  434. package/dist/rfc64/private-catalog-subscription-readiness-v1.d.ts +33 -0
  435. package/dist/rfc64/private-catalog-subscription-readiness-v1.d.ts.map +1 -0
  436. package/dist/rfc64/private-catalog-subscription-readiness-v1.js +97 -0
  437. package/dist/rfc64/private-catalog-subscription-readiness-v1.js.map +1 -0
  438. package/dist/rfc64/private-read-roster-v1.d.ts +14 -0
  439. package/dist/rfc64/private-read-roster-v1.d.ts.map +1 -0
  440. package/dist/rfc64/private-read-roster-v1.js +57 -0
  441. package/dist/rfc64/private-read-roster-v1.js.map +1 -0
  442. package/dist/rfc64/public-catalog-native-receiver-v1.d.ts.map +1 -1
  443. package/dist/rfc64/public-catalog-native-receiver-v1.js +26 -21
  444. package/dist/rfc64/public-catalog-native-receiver-v1.js.map +1 -1
  445. package/dist/rfc64/public-catalog-native-reconciler-v1.d.ts +17 -11
  446. package/dist/rfc64/public-catalog-native-reconciler-v1.d.ts.map +1 -1
  447. package/dist/rfc64/public-catalog-native-reconciler-v1.js +10 -8
  448. package/dist/rfc64/public-catalog-native-reconciler-v1.js.map +1 -1
  449. package/dist/rfc64/public-catalog-receiver-v1.d.ts +9 -0
  450. package/dist/rfc64/public-catalog-receiver-v1.d.ts.map +1 -1
  451. package/dist/rfc64/public-catalog-receiver-v1.js +11 -2
  452. package/dist/rfc64/public-catalog-receiver-v1.js.map +1 -1
  453. package/dist/rfc64/public-catalog-service-v1.d.ts.map +1 -1
  454. package/dist/rfc64/public-catalog-service-v1.js +21 -22
  455. package/dist/rfc64/public-catalog-service-v1.js.map +1 -1
  456. package/dist/rfc64/public-catalog-successor-producer-v1.d.ts.map +1 -1
  457. package/dist/rfc64/public-catalog-successor-producer-v1.js +5 -4
  458. package/dist/rfc64/public-catalog-successor-producer-v1.js.map +1 -1
  459. package/dist/rfc64/registered-private-empty-vm-proof-v1.d.ts +20 -0
  460. package/dist/rfc64/registered-private-empty-vm-proof-v1.d.ts.map +1 -0
  461. package/dist/rfc64/registered-private-empty-vm-proof-v1.js +26 -0
  462. package/dist/rfc64/registered-private-empty-vm-proof-v1.js.map +1 -0
  463. package/dist/rfc64/swm-catalog-durable-asset-resolver-v1.d.ts.map +1 -1
  464. package/dist/rfc64/swm-catalog-durable-asset-resolver-v1.js +60 -53
  465. package/dist/rfc64/swm-catalog-durable-asset-resolver-v1.js.map +1 -1
  466. package/dist/rfc64/swm-inventory-catalog-reconciler-v1.d.ts +2 -1
  467. package/dist/rfc64/swm-inventory-catalog-reconciler-v1.d.ts.map +1 -1
  468. package/dist/rfc64/swm-inventory-catalog-reconciler-v1.js +44 -4
  469. package/dist/rfc64/swm-inventory-catalog-reconciler-v1.js.map +1 -1
  470. package/dist/rfc64/swm-inventory-shadow-runtime-v1.d.ts +3 -1
  471. package/dist/rfc64/swm-inventory-shadow-runtime-v1.d.ts.map +1 -1
  472. package/dist/rfc64/swm-inventory-shadow-runtime-v1.js +34 -0
  473. package/dist/rfc64/swm-inventory-shadow-runtime-v1.js.map +1 -1
  474. package/dist/sync/auth/pipe-request-tail.d.ts +5 -1
  475. package/dist/sync/auth/pipe-request-tail.d.ts.map +1 -1
  476. package/dist/sync/auth/pipe-request-tail.js +13 -3
  477. package/dist/sync/auth/pipe-request-tail.js.map +1 -1
  478. package/dist/sync/auth/request-build.d.ts +3 -0
  479. package/dist/sync/auth/request-build.d.ts.map +1 -1
  480. package/dist/sync/auth/request-build.js +11 -2
  481. package/dist/sync/auth/request-build.js.map +1 -1
  482. package/dist/sync/backpressure.d.ts +27 -16
  483. package/dist/sync/backpressure.d.ts.map +1 -1
  484. package/dist/sync/backpressure.js +66 -40
  485. package/dist/sync/backpressure.js.map +1 -1
  486. package/dist/sync/catchup-policy.d.ts +2 -0
  487. package/dist/sync/catchup-policy.d.ts.map +1 -1
  488. package/dist/sync/catchup-policy.js +2 -0
  489. package/dist/sync/catchup-policy.js.map +1 -1
  490. package/dist/sync/exact-asset-fetch.d.ts +22 -0
  491. package/dist/sync/exact-asset-fetch.d.ts.map +1 -1
  492. package/dist/sync/exact-asset-fetch.js +30 -3
  493. package/dist/sync/exact-asset-fetch.js.map +1 -1
  494. package/dist/sync/exact-assets.d.ts +7 -5
  495. package/dist/sync/exact-assets.d.ts.map +1 -1
  496. package/dist/sync/exact-assets.js +13 -8
  497. package/dist/sync/exact-assets.js.map +1 -1
  498. package/dist/sync/exact-batch-observation.d.ts +3 -0
  499. package/dist/sync/exact-batch-observation.d.ts.map +1 -0
  500. package/dist/sync/exact-batch-observation.js +13 -0
  501. package/dist/sync/exact-batch-observation.js.map +1 -0
  502. package/dist/sync/exact-batch-stream-capability.d.ts +24 -0
  503. package/dist/sync/exact-batch-stream-capability.d.ts.map +1 -0
  504. package/dist/sync/exact-batch-stream-capability.js +88 -0
  505. package/dist/sync/exact-batch-stream-capability.js.map +1 -0
  506. package/dist/sync/exact-batch-stream-contract.d.ts +78 -0
  507. package/dist/sync/exact-batch-stream-contract.d.ts.map +1 -0
  508. package/dist/sync/exact-batch-stream-contract.js +206 -0
  509. package/dist/sync/exact-batch-stream-contract.js.map +1 -0
  510. package/dist/sync/on-connect/sync-on-connect.d.ts +1 -1
  511. package/dist/sync/on-connect/sync-on-connect.d.ts.map +1 -1
  512. package/dist/sync/on-connect/sync-on-connect.js +3 -3
  513. package/dist/sync/on-connect/sync-on-connect.js.map +1 -1
  514. package/dist/sync/on-demand-agents-phonebook.d.ts +162 -0
  515. package/dist/sync/on-demand-agents-phonebook.d.ts.map +1 -0
  516. package/dist/sync/on-demand-agents-phonebook.js +571 -0
  517. package/dist/sync/on-demand-agents-phonebook.js.map +1 -0
  518. package/dist/sync/peer-connection.d.ts.map +1 -1
  519. package/dist/sync/peer-connection.js +11 -2
  520. package/dist/sync/peer-connection.js.map +1 -1
  521. package/dist/sync/priority-admission-queue.d.ts +17 -0
  522. package/dist/sync/priority-admission-queue.d.ts.map +1 -1
  523. package/dist/sync/priority-admission-queue.js +58 -37
  524. package/dist/sync/priority-admission-queue.js.map +1 -1
  525. package/dist/sync/requester/admission-boundary.d.ts +12 -0
  526. package/dist/sync/requester/admission-boundary.d.ts.map +1 -0
  527. package/dist/sync/requester/admission-boundary.js +22 -0
  528. package/dist/sync/requester/admission-boundary.js.map +1 -0
  529. package/dist/sync/requester/durable-sync.d.ts +2 -1
  530. package/dist/sync/requester/durable-sync.d.ts.map +1 -1
  531. package/dist/sync/requester/durable-sync.js +3 -188
  532. package/dist/sync/requester/durable-sync.js.map +1 -1
  533. package/dist/sync/requester/exact-batch-stream-driver.d.ts +83 -0
  534. package/dist/sync/requester/exact-batch-stream-driver.d.ts.map +1 -0
  535. package/dist/sync/requester/exact-batch-stream-driver.js +299 -0
  536. package/dist/sync/requester/exact-batch-stream-driver.js.map +1 -0
  537. package/dist/sync/requester/exact-batch-stream.d.ts +72 -0
  538. package/dist/sync/requester/exact-batch-stream.d.ts.map +1 -0
  539. package/dist/sync/requester/exact-batch-stream.js +267 -0
  540. package/dist/sync/requester/exact-batch-stream.js.map +1 -0
  541. package/dist/sync/requester/exact-recovery-transport.d.ts +19 -0
  542. package/dist/sync/requester/exact-recovery-transport.d.ts.map +1 -0
  543. package/dist/sync/requester/exact-recovery-transport.js +3 -0
  544. package/dist/sync/requester/exact-recovery-transport.js.map +1 -0
  545. package/dist/sync/requester/finalized-swm-twin-reconciliation.js +12 -3
  546. package/dist/sync/requester/finalized-swm-twin-reconciliation.js.map +1 -1
  547. package/dist/sync/requester/graph-scoped-materialization.d.ts.map +1 -1
  548. package/dist/sync/requester/graph-scoped-materialization.js +61 -27
  549. package/dist/sync/requester/graph-scoped-materialization.js.map +1 -1
  550. package/dist/sync/requester/page-fetch.d.ts +7 -0
  551. package/dist/sync/requester/page-fetch.d.ts.map +1 -1
  552. package/dist/sync/requester/page-fetch.js +103 -49
  553. package/dist/sync/requester/page-fetch.js.map +1 -1
  554. package/dist/sync/requester/shared-memory-sync.d.ts +10 -5
  555. package/dist/sync/requester/shared-memory-sync.d.ts.map +1 -1
  556. package/dist/sync/requester/shared-memory-sync.js +149 -150
  557. package/dist/sync/requester/shared-memory-sync.js.map +1 -1
  558. package/dist/sync/requester/swm-materialization-memo.d.ts +12 -0
  559. package/dist/sync/requester/swm-materialization-memo.d.ts.map +1 -0
  560. package/dist/sync/requester/swm-materialization-memo.js +54 -0
  561. package/dist/sync/requester/swm-materialization-memo.js.map +1 -0
  562. package/dist/sync/requester/swm-recovery.d.ts +3 -29
  563. package/dist/sync/requester/swm-recovery.d.ts.map +1 -1
  564. package/dist/sync/requester/swm-recovery.js +11 -10
  565. package/dist/sync/requester/swm-recovery.js.map +1 -1
  566. package/dist/sync/requester/swm-snapshot-materializer.d.ts.map +1 -1
  567. package/dist/sync/requester/swm-snapshot-materializer.js +18 -0
  568. package/dist/sync/requester/swm-snapshot-materializer.js.map +1 -1
  569. package/dist/sync/requester/swm-target-executor.d.ts +6 -0
  570. package/dist/sync/requester/swm-target-executor.d.ts.map +1 -1
  571. package/dist/sync/requester/swm-target-executor.js +24 -3
  572. package/dist/sync/requester/swm-target-executor.js.map +1 -1
  573. package/dist/sync/requester/verified-asset-preparation.d.ts +9 -0
  574. package/dist/sync/requester/verified-asset-preparation.d.ts.map +1 -0
  575. package/dist/sync/requester/verified-asset-preparation.js +190 -0
  576. package/dist/sync/requester/verified-asset-preparation.js.map +1 -0
  577. package/dist/sync/responder/asset-metadata.d.ts +18 -0
  578. package/dist/sync/responder/asset-metadata.d.ts.map +1 -0
  579. package/dist/sync/responder/asset-metadata.js +27 -0
  580. package/dist/sync/responder/asset-metadata.js.map +1 -0
  581. package/dist/sync/responder/durable-data-request-policy.d.ts +2 -16
  582. package/dist/sync/responder/durable-data-request-policy.d.ts.map +1 -1
  583. package/dist/sync/responder/durable-data-request-policy.js +1 -31
  584. package/dist/sync/responder/durable-data-request-policy.js.map +1 -1
  585. package/dist/sync/responder/exact-asset-export-cache.d.ts +89 -0
  586. package/dist/sync/responder/exact-asset-export-cache.d.ts.map +1 -0
  587. package/dist/sync/responder/exact-asset-export-cache.js +550 -0
  588. package/dist/sync/responder/exact-asset-export-cache.js.map +1 -0
  589. package/dist/sync/responder/exact-batch-stream.d.ts +47 -0
  590. package/dist/sync/responder/exact-batch-stream.d.ts.map +1 -0
  591. package/dist/sync/responder/exact-batch-stream.js +151 -0
  592. package/dist/sync/responder/exact-batch-stream.js.map +1 -0
  593. package/dist/sync/responder/exact-data-session.d.ts +67 -0
  594. package/dist/sync/responder/exact-data-session.d.ts.map +1 -0
  595. package/dist/sync/responder/exact-data-session.js +130 -0
  596. package/dist/sync/responder/exact-data-session.js.map +1 -0
  597. package/dist/sync/responder/exact-graph-cursor-filter.d.ts +9 -0
  598. package/dist/sync/responder/exact-graph-cursor-filter.d.ts.map +1 -0
  599. package/dist/sync/responder/exact-graph-cursor-filter.js +101 -0
  600. package/dist/sync/responder/exact-graph-cursor-filter.js.map +1 -0
  601. package/dist/sync/responder/exact-graph-reader.d.ts +95 -0
  602. package/dist/sync/responder/exact-graph-reader.d.ts.map +1 -0
  603. package/dist/sync/responder/exact-graph-reader.js +623 -0
  604. package/dist/sync/responder/exact-graph-reader.js.map +1 -0
  605. package/dist/sync/responder/exact-page-reader.d.ts +37 -0
  606. package/dist/sync/responder/exact-page-reader.d.ts.map +1 -0
  607. package/dist/sync/responder/exact-page-reader.js +130 -0
  608. package/dist/sync/responder/exact-page-reader.js.map +1 -0
  609. package/dist/sync/responder/graph-plan.d.ts +19 -85
  610. package/dist/sync/responder/graph-plan.d.ts.map +1 -1
  611. package/dist/sync/responder/graph-plan.js +51 -988
  612. package/dist/sync/responder/graph-plan.js.map +1 -1
  613. package/dist/sync/responder/page-framing-policy.d.ts +31 -0
  614. package/dist/sync/responder/page-framing-policy.d.ts.map +1 -0
  615. package/dist/sync/responder/page-framing-policy.js +41 -0
  616. package/dist/sync/responder/page-framing-policy.js.map +1 -0
  617. package/dist/sync/responder/responder-row-page.d.ts +85 -0
  618. package/dist/sync/responder/responder-row-page.d.ts.map +1 -0
  619. package/dist/sync/responder/responder-row-page.js +181 -0
  620. package/dist/sync/responder/responder-row-page.js.map +1 -0
  621. package/dist/sync/responder/row-serialization.d.ts +34 -0
  622. package/dist/sync/responder/row-serialization.d.ts.map +1 -0
  623. package/dist/sync/responder/row-serialization.js +69 -0
  624. package/dist/sync/responder/row-serialization.js.map +1 -0
  625. package/dist/sync/responder/session-plan-memo.d.ts +20 -0
  626. package/dist/sync/responder/session-plan-memo.d.ts.map +1 -0
  627. package/dist/sync/responder/session-plan-memo.js +135 -0
  628. package/dist/sync/responder/session-plan-memo.js.map +1 -0
  629. package/dist/sync/responder/sync-handler.d.ts +26 -1
  630. package/dist/sync/responder/sync-handler.d.ts.map +1 -1
  631. package/dist/sync/responder/sync-handler.js +107 -58
  632. package/dist/sync/responder/sync-handler.js.map +1 -1
  633. package/dist/sync/shared-memory-completion.d.ts +30 -0
  634. package/dist/sync/shared-memory-completion.d.ts.map +1 -1
  635. package/dist/sync/shared-memory-completion.js +13 -0
  636. package/dist/sync/shared-memory-completion.js.map +1 -1
  637. package/dist/sync/system-context-graph-policy.d.ts +10 -0
  638. package/dist/sync/system-context-graph-policy.d.ts.map +1 -1
  639. package/dist/sync/system-context-graph-policy.js +11 -0
  640. package/dist/sync/system-context-graph-policy.js.map +1 -1
  641. package/dist/sync/wire-compression.d.ts +47 -0
  642. package/dist/sync/wire-compression.d.ts.map +1 -0
  643. package/dist/sync/wire-compression.js +111 -0
  644. package/dist/sync/wire-compression.js.map +1 -0
  645. package/dist/unscoped-query-consistency.d.ts +12 -0
  646. package/dist/unscoped-query-consistency.d.ts.map +1 -1
  647. package/dist/unscoped-query-consistency.js +17 -1
  648. package/dist/unscoped-query-consistency.js.map +1 -1
  649. package/dist/vm-promotion-audit.d.ts +2 -2
  650. package/dist/vm-promotion-audit.d.ts.map +1 -1
  651. package/dist/vm-promotion-audit.js +26 -9
  652. package/dist/vm-promotion-audit.js.map +1 -1
  653. package/dist/vm-publish-snapshot-preflight.d.ts +13 -0
  654. package/dist/vm-publish-snapshot-preflight.d.ts.map +1 -0
  655. package/dist/vm-publish-snapshot-preflight.js +94 -0
  656. package/dist/vm-publish-snapshot-preflight.js.map +1 -0
  657. package/dist/vm-reconcile-dispatcher.d.ts +42 -1
  658. package/dist/vm-reconcile-dispatcher.d.ts.map +1 -1
  659. package/dist/vm-reconcile-dispatcher.js +103 -4
  660. package/dist/vm-reconcile-dispatcher.js.map +1 -1
  661. package/dist/vm-reconcile-peer-topology.d.ts +7 -9
  662. package/dist/vm-reconcile-peer-topology.d.ts.map +1 -1
  663. package/dist/vm-reconcile-peer-topology.js +1 -37
  664. package/dist/vm-reconcile-peer-topology.js.map +1 -1
  665. package/dist/vm-recovery-candidate-order.d.ts +16 -0
  666. package/dist/vm-recovery-candidate-order.d.ts.map +1 -0
  667. package/dist/vm-recovery-candidate-order.js +24 -0
  668. package/dist/vm-recovery-candidate-order.js.map +1 -0
  669. package/dist/vm-recovery-core-transport-preference.d.ts +69 -0
  670. package/dist/vm-recovery-core-transport-preference.d.ts.map +1 -0
  671. package/dist/vm-recovery-core-transport-preference.js +131 -0
  672. package/dist/vm-recovery-core-transport-preference.js.map +1 -0
  673. package/dist/vm-recovery-experiment-policy.d.ts +24 -0
  674. package/dist/vm-recovery-experiment-policy.d.ts.map +1 -0
  675. package/dist/vm-recovery-experiment-policy.js +18 -0
  676. package/dist/vm-recovery-experiment-policy.js.map +1 -0
  677. package/dist/vm-recovery-footprint.d.ts +54 -0
  678. package/dist/vm-recovery-footprint.d.ts.map +1 -1
  679. package/dist/vm-recovery-footprint.js +88 -24
  680. package/dist/vm-recovery-footprint.js.map +1 -1
  681. package/dist/vm-recovery-microbatch.d.ts +2 -0
  682. package/dist/vm-recovery-microbatch.d.ts.map +1 -1
  683. package/dist/vm-recovery-microbatch.js +7 -3
  684. package/dist/vm-recovery-microbatch.js.map +1 -1
  685. package/dist/vm-recovery-pass-authority.d.ts +84 -0
  686. package/dist/vm-recovery-pass-authority.d.ts.map +1 -0
  687. package/dist/vm-recovery-pass-authority.js +126 -0
  688. package/dist/vm-recovery-pass-authority.js.map +1 -0
  689. package/dist/vm-recovery-phase-timing.d.ts +75 -0
  690. package/dist/vm-recovery-phase-timing.d.ts.map +1 -0
  691. package/dist/vm-recovery-phase-timing.js +236 -0
  692. package/dist/vm-recovery-phase-timing.js.map +1 -0
  693. package/dist/vm-recovery-preparation.d.ts +143 -0
  694. package/dist/vm-recovery-preparation.d.ts.map +1 -0
  695. package/dist/vm-recovery-preparation.js +433 -0
  696. package/dist/vm-recovery-preparation.js.map +1 -0
  697. package/dist/vm-recovery-provider-policy.d.ts +13 -1
  698. package/dist/vm-recovery-provider-policy.d.ts.map +1 -1
  699. package/dist/vm-recovery-provider-policy.js +37 -5
  700. package/dist/vm-recovery-provider-policy.js.map +1 -1
  701. package/dist/vm-recovery-stream-profile.d.ts +15 -0
  702. package/dist/vm-recovery-stream-profile.d.ts.map +1 -0
  703. package/dist/vm-recovery-stream-profile.js +53 -0
  704. package/dist/vm-recovery-stream-profile.js.map +1 -0
  705. package/dist/vm-recovery-stream-setback-policy.d.ts +54 -0
  706. package/dist/vm-recovery-stream-setback-policy.d.ts.map +1 -0
  707. package/dist/vm-recovery-stream-setback-policy.js +85 -0
  708. package/dist/vm-recovery-stream-setback-policy.js.map +1 -0
  709. package/dist/vm-recovery-timing-observer.d.ts +27 -0
  710. package/dist/vm-recovery-timing-observer.d.ts.map +1 -0
  711. package/dist/vm-recovery-timing-observer.js +49 -0
  712. package/dist/vm-recovery-timing-observer.js.map +1 -0
  713. package/dist/vm-recovery-transport-budget-policy.d.ts +32 -0
  714. package/dist/vm-recovery-transport-budget-policy.d.ts.map +1 -0
  715. package/dist/vm-recovery-transport-budget-policy.js +55 -0
  716. package/dist/vm-recovery-transport-budget-policy.js.map +1 -0
  717. package/dist/vm-recovery-transport-plan.d.ts +83 -0
  718. package/dist/vm-recovery-transport-plan.d.ts.map +1 -0
  719. package/dist/vm-recovery-transport-plan.js +117 -0
  720. package/dist/vm-recovery-transport-plan.js.map +1 -0
  721. package/dist/vm-refresh.d.ts +138 -0
  722. package/dist/vm-refresh.d.ts.map +1 -0
  723. package/dist/vm-refresh.js +201 -0
  724. package/dist/vm-refresh.js.map +1 -0
  725. package/package.json +19 -9
  726. package/dist/finalization-annotations.d.ts +0 -5
  727. package/dist/finalization-annotations.d.ts.map +0 -1
  728. package/dist/finalization-annotations.js +0 -6
  729. package/dist/finalization-annotations.js.map +0 -1
  730. package/dist/swm-ka-bound.d.ts +0 -21
  731. package/dist/swm-ka-bound.d.ts.map +0 -1
  732. package/dist/swm-ka-bound.js +0 -36
  733. package/dist/swm-ka-bound.js.map +0 -1
@@ -6,12 +6,16 @@
6
6
  * the catchup/enable/stats entrypoints. Bodies are a 1:1 move; methods take
7
7
  * `this: DKGAgent` so cross-calls resolve against the composed class.
8
8
  */
9
+ import { orderVmRecoveryCandidates } from './vm-recovery-candidate-order.js';
9
10
  import { createHash } from 'node:crypto';
11
+ import { createSwmHostModeHandler } from './internal/gossip/host-mode-handler.js';
10
12
  import { Buffer } from 'node:buffer';
13
+ import { VmRecoveryCoreTransportPreferencePolicy } from './vm-recovery-core-transport-preference.js';
14
+ import { VmRecoveryPassAuthority } from './vm-recovery-pass-authority.js';
11
15
  import { performance } from 'node:perf_hooks';
12
- import { PROTOCOL_GET_CIPHERTEXT_CHUNK, PROTOCOL_SWM_HOST_CATCHUP, contextGraphWorkspaceTopic, contextGraphWorkspaceGraphUri, contextGraphWorkspaceMetaGraphUri, contextGraphDataUri, contextGraphMetaUri, contextGraphLayerUri, MemoryLayer, GOSSIP_TYPE_WORKSPACE_PUBLISH, decodeGossipEnvelope, decodeEncryptedWorkspacePayload, ENCRYPTED_WORKSPACE_ENVELOPE_TYPE, decodeSwmSenderKeyMessage, SWM_SENDER_KEY_MESSAGE_TYPE, SYSTEM_CONTEXT_GRAPHS, createOperationContext, getMetrics, sparqlString, isSafeIri, assertSafeIri, TRUST_LEVEL_PREDICATE, LEGACY_TRUST_LEVEL_PREDICATE, GOSSIP_TYPE_WORKSPACE_PUBLISH_CHUNKED, ciphertextChunkStoreGraph, ciphertextChunkStoreSubject, CIPHERTEXT_CHUNK_PREDICATE, SUBSCRIPTION_SOURCES, } from '@origintrail-official/dkg-core';
13
- import { GraphManager, isStoreSchedulerBusyError, withDefaultStoreWorkPriority, asChangelogReader, asGraphWriteRevisionSource, tryUpdateWithTouchedGraphs } from '@origintrail-official/dkg-storage';
14
- import { readMaterializedVersion, shouldApplyMaterialization, withMaterializationLock, isKnowledgeAssetWorkspaceHeadCorruptError, } from '@origintrail-official/dkg-publisher';
16
+ import { PROTOCOL_GET_CIPHERTEXT_CHUNK, PROTOCOL_SWM_HOST_CATCHUP, contextGraphWorkspaceTopic, contextGraphDataUri, contextGraphMetaUri, contextGraphLayerUri, MemoryLayer, GOSSIP_TYPE_WORKSPACE_PUBLISH, decodeGossipEnvelope, decodeEncryptedWorkspacePayload, ENCRYPTED_WORKSPACE_ENVELOPE_TYPE, decodeSwmSenderKeyMessage, SWM_SENDER_KEY_MESSAGE_TYPE, SYSTEM_CONTEXT_GRAPHS, createOperationContext, sparqlString, isSafeIri, assertSafeIri, TRUST_LEVEL_PREDICATE, LEGACY_TRUST_LEVEL_PREDICATE, GOSSIP_TYPE_WORKSPACE_PUBLISH_CHUNKED, ciphertextChunkStoreGraph, ciphertextChunkStoreSubject, CIPHERTEXT_CHUNK_PREDICATE, SUBSCRIPTION_SOURCES, } from '@origintrail-official/dkg-core';
17
+ import { GraphManager, isStoreSchedulerBusyError, withDefaultStoreWorkPriority, tryUpdateWithTouchedGraphs } from '@origintrail-official/dkg-storage';
18
+ import { readMaterializedVersion, shouldApplyMaterialization, withMaterializationLock, isKnowledgeAssetWorkspaceHeadCorruptError, readConfirmedGraphKnowledgeAssetMetadataEnvelope, resolveKnowledgeAssetWorkspaceHead, } from '@origintrail-official/dkg-publisher';
15
19
  import { ethers } from 'ethers';
16
20
  import { isCoreHostedPublicCgRecorded, resolveCoreHostedPublicCgLocalId, } from './core-hosted-public-cg-record-decision.js';
17
21
  import { buildReconciledKnowledgeAssetUal, } from './ka-identity.js';
@@ -24,12 +28,20 @@ import { decodeCiphertextChunkCatchupRequest, encodeCiphertextChunkCatchupReques
24
28
  import { orderCatchupPeers } from './p2p/peer-selection.js';
25
29
  import { reconcileContextGraph, VmReconcileSchedulingRuntime, } from './chain-reconciler.js';
26
30
  import { ContextGraphOnChainIdUnresolvedError, VmReconcileUnavailableError, VmReconcileQueueClosedError, } from './vm-reconcile-service.js';
31
+ import { isUnansweredVmReconcileReadAuthority, VmReconcileReadAuthorityUnansweredError, } from './internal/vm-reconcile-read-authority.js';
27
32
  import { createCursorState } from './reconcile-cursor.js';
28
- import { planVmRecoveryMicrobatch, } from './vm-recovery-microbatch.js';
29
- import { enrichVmRecoveryFootprints } from './vm-recovery-footprint.js';
33
+ import { resolveVmRecoveryExperimentPolicy } from './vm-recovery-experiment-policy.js';
34
+ import { VmRecoveryTimingObserver } from './vm-recovery-timing-observer.js';
35
+ import { noteVmReconcilePassEnd, observeVmRecoveryTiming, vmReconcilePassGapMs, } from './vm-recovery-phase-timing.js';
36
+ import { existingVmRecoveryPreparation, vmRecoveryPreparationFor, } from './vm-recovery-preparation.js';
37
+ import { planVmRecoveryTransport, VmRecoveryTransportPreparation, VM_EXACT_MICROBATCH_LIMITS } from './vm-recovery-transport-plan.js';
38
+ import { EXACT_BATCH_STREAM_PROTOCOL } from './sync/exact-batch-stream-contract.js';
39
+ import { exactBatchStreamUnsupported } from './sync/exact-batch-stream-capability.js';
30
40
  import { VmRecoveryProviderPolicy, } from './vm-recovery-provider-policy.js';
31
- import { encodeExactAssetUals, MAX_EXACT_SYNC_ASSETS, } from './sync/exact-assets.js';
32
- import { MAX_CONTEXT_GRAPH_ASSET_FETCH_PEERS, ExactAssetFetchLifecycleClosedError, runExactAssetFetch, } from './sync/exact-asset-fetch.js';
41
+ import { MAX_CONTEXT_GRAPH_ASSET_FETCH_PEERS, ExactAssetFetchLifecycleClosedError, ExactAssetVersionBehindError, exactAssetFetchPeerWindow, runExactAssetFetch, } from './sync/exact-asset-fetch.js';
42
+ import { isBoundedOperationTimeoutError, runBoundedOperation } from './bounded-operation.js';
43
+ /** Graph-scoped KA metadata marker; its presence names the metadata graph. */
44
+ const GRAPH_KA_CONTENT_SCOPE_VERSION_PREDICATE = 'http://dkg.io/ontology/contentScopeVersion';
33
45
  function rsHealStoreOptions(operation, signal) {
34
46
  return {
35
47
  priority: 'background',
@@ -73,12 +85,13 @@ function advanceRsHealCursor(cursorMap, cursorKey, bindings, batchSize, maxEntri
73
85
  cursorMap.delete(oldest);
74
86
  }
75
87
  }
76
- import { canReuseVmReconcilePeerTopology, createVmReconcileCleanMissPeerIds, createVmReconcilePeerTopology, encodeLegacyVmReconcilePeerTopologyKey, isVmReconcilePeerTopology, parseLegacyVmReconcilePeerTopologyKey, parseVmReconcileCleanMissPeerIds, UNREADABLE_VM_RECONCILE_PEER_TOPOLOGY, } from './vm-reconcile-peer-topology.js';
77
- import { TIMEOUT_SENTINEL, } from './dkg-agent-constants.js';
88
+ import { SYNC_MIN_GRAPH_BUDGET_MS, TIMEOUT_SENTINEL, } from './dkg-agent-constants.js';
78
89
  import { chainAuthorityReadBudgetsOf } from './chain-authority-read-budgets.js';
79
90
  import { ContextGraphNotFoundError, } from './dkg-agent-types.js';
80
91
  import { DKGAgentBase } from './dkg-agent-base.js';
81
- import { resolveVmReconcilerEnabled } from './sync/backpressure.js';
92
+ import { resolveExactBatchStreamEnabled, resolveVmReconcilerEnabled } from './sync/backpressure.js';
93
+ import { mapWithConcurrencyDrained } from './map-with-concurrency.js';
94
+ import { VM_RECOVERY_SYNC_PRIORITY } from './sync/catchup-policy.js';
82
95
  import { finalizedContextGraphSnapshotMismatchV1 } from './internal/context-graph-authority/finalized-context-graph-binding.js';
83
96
  import { CONTEXT_GRAPH_AUTHORITY_RPC_SITES as CG_AUTH_RPC_SITES, withOwnedRpcRequestContext, withRpcUsageSite, } from '@origintrail-official/dkg-chain';
84
97
  const DEFAULT_HOST_MODE_RECONCILE_BATCH_SIZE = 32;
@@ -105,26 +118,6 @@ function vmReconcileDeploymentMatches(value, expected) {
105
118
  * amplify into a per-message `eth_call` (Branimir review #1239 follow-on).
106
119
  */
107
120
  const HOST_MODE_PUBLISH_POLICY_MAX_CACHE_AGE_MS = 5_000;
108
- // Exact VM data responses are page-only and capped at 64 rows per page. Keep
109
- // one recovery microbatch near 64 non-empty pages so a single large graph does
110
- // not monopolize the global sync admission. This is a soft scheduling/fairness
111
- // cap, not a wire or correctness limit: an individually larger KA is still
112
- // admitted alone and remains bounded by the exact executor's hard guards.
113
- const VM_EXACT_MICROBATCH_PAGE_FAIRNESS_LEAVES = 4096n;
114
- const VM_EXACT_MICROBATCH_LIMITS = Object.freeze({
115
- // Executor capability: the current exact-sync envelope accepts at most ten.
116
- maxAssets: MAX_EXACT_SYNC_ASSETS,
117
- // Soft recovery-window targets. A single public KA may exceed either; it is
118
- // still admitted alone while the exact executor's own hard byte/quad guards
119
- // remain authoritative.
120
- targetBytes: 24n * 1024n * 1024n,
121
- targetLeaves: VM_EXACT_MICROBATCH_PAGE_FAIRNESS_LEAVES,
122
- fixedBytesPerAsset: 64n * 1024n,
123
- bytesPerLeafOverhead: 128n,
124
- byteSizeMultiplierBps: 11500n,
125
- // Hard exact-selector cap, evaluated with the executor's real encoder.
126
- maxSelectorBytes: 16 * 1024,
127
- });
128
121
  /**
129
122
  * Recovery-target root of an ordinal whose KA the node holds nowhere locally:
130
123
  * the sweep queues it without reading the chain root. Exact recovery fetches
@@ -174,6 +167,13 @@ async function raceVmReconcileAbort(work, signal) {
174
167
  signal.removeEventListener('abort', onAbort);
175
168
  }
176
169
  }
170
+ /** `peerIds` started `offset` places in, wrapping. */
171
+ function rotatePeerIds(peerIds, offset) {
172
+ if (peerIds.length === 0)
173
+ return [];
174
+ const start = offset % peerIds.length;
175
+ return [...peerIds.slice(start), ...peerIds.slice(0, start)];
176
+ }
177
177
  /** Track the raw dependency even if an abort race releases its caller first. */
178
178
  function trackVmReconcilePhysicalRun(runs, run) {
179
179
  runs.add(run);
@@ -312,6 +312,10 @@ export class SwmHostModeMethods extends DKGAgentBase {
312
312
  * reserved, or when the node is already subscribed in member mode.
313
313
  */
314
314
  async reconcileSwmHostModeSubscription(contextGraphId, source = SUBSCRIPTION_SOURCES.RECONCILER) {
315
+ const session = this.gossipSession;
316
+ const live = session.live();
317
+ if (live === null)
318
+ return;
315
319
  if (!this.swmHostModeStore)
316
320
  return;
317
321
  if (Object.values(SYSTEM_CONTEXT_GRAPHS).includes(contextGraphId))
@@ -347,7 +351,9 @@ export class SwmHostModeMethods extends DKGAgentBase {
347
351
  await this.isCuratedForHostMode(contextGraphId)) {
348
352
  // A manually hosted CG can become curated later. Upgrade the cached
349
353
  // classification so the existing handler starts stripping immediately.
350
- this.swmHostModeCurated.set(hostKey, true);
354
+ if (!session.active || this.gossipSession !== session)
355
+ return;
356
+ session.swmHostModeCurated.set(hostKey, true);
351
357
  }
352
358
  await this.maybeMarkRegisteredForHostMode(contextGraphId);
353
359
  return;
@@ -382,7 +388,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
382
388
  // critically, the host-only-core case where there's no local
383
389
  // `_meta` and `isPrivateContextGraph` alone returns false.
384
390
  const curated = await this.isCuratedForHostMode(contextGraphId);
385
- if (!curated)
391
+ if (!curated || !session.active || this.gossipSession !== session)
386
392
  return;
387
393
  // OT-RFC-49 WS-A — the private-ciphertext strip. With `stripCiphertext`
388
394
  // ON (default), a core declines ALL host-mode custody for a curated CG:
@@ -502,15 +508,19 @@ export class SwmHostModeMethods extends DKGAgentBase {
502
508
  * so the host-mode lifecycle is in one place.
503
509
  */
504
510
  wireSwmHostModeHandler(contextGraphId, source = SUBSCRIPTION_SOURCES.RECONCILER, curated = true) {
511
+ const session = this.gossipSession;
512
+ const live = session.live();
513
+ if (live === null)
514
+ return;
505
515
  if (!this.rfc64LegacySwmGossipAllowedForContextGraph(contextGraphId)) {
506
516
  const hostKey = this.canonicalSwmHostModeKey(contextGraphId);
507
- const hadRuntimeHostState = this.swmHostModeHandlers.has(hostKey)
508
- || this.swmHostModeSubscribed.has(hostKey)
509
- || this.swmHostModeCurated.has(hostKey);
517
+ const hadRuntimeHostState = session.swmHostModeHandlers.has(hostKey)
518
+ || session.swmHostModeSubscribed.has(hostKey)
519
+ || session.swmHostModeCurated.has(hostKey);
510
520
  this.unwireSwmHostModeHandler(contextGraphId);
511
- const deletedStaleHandler = this.swmHostModeHandlers.delete(hostKey);
512
- const deletedStaleSubscription = this.swmHostModeSubscribed.delete(hostKey);
513
- const deletedStaleClassification = this.swmHostModeCurated.delete(hostKey);
521
+ const deletedStaleHandler = session.swmHostModeHandlers.delete(hostKey);
522
+ const deletedStaleSubscription = session.swmHostModeSubscribed.delete(hostKey);
523
+ const deletedStaleClassification = session.swmHostModeCurated.delete(hostKey);
514
524
  if (deletedStaleHandler || deletedStaleSubscription || deletedStaleClassification) {
515
525
  // Heal partially restored/stale bookkeeping even when its handler
516
526
  // reference is absent, which makes unwireSwmHostModeHandler a no-op.
@@ -538,7 +548,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
538
548
  // genuine no-op (instead of silently wiring a second handler on
539
549
  // the same topic).
540
550
  const wireCgId = this.canonicalSwmHostModeKey(contextGraphId);
541
- if (this.swmHostModeHandlers.has(wireCgId)) {
551
+ if (session.swmHostModeHandlers.has(wireCgId)) {
542
552
  // Idempotent re-entry — preserve the original source. The first
543
553
  // discovery path to wire the handler wins the provenance label;
544
554
  // a later path covering the same CG is "also true" but the
@@ -546,44 +556,12 @@ export class SwmHostModeMethods extends DKGAgentBase {
546
556
  return;
547
557
  }
548
558
  const swmTopic = contextGraphWorkspaceTopic(wireCgId);
549
- this.swmHostModeSubscribed.set(wireCgId, source);
550
- this.swmHostModeCurated.set(wireCgId, curated);
551
- this.gossip.subscribe(swmTopic);
552
- const handler = (_topic, data, from) => {
553
- // Fail closed when the classification is absent. Only an explicitly
554
- // non-curated manual subscription retains the public host-mode hatch.
555
- if (this.swmHostModeStripCiphertext() &&
556
- this.swmHostModeCurated.get(wireCgId) !== false) {
557
- this.log.debug(createOperationContext('share'), `Dropping host-mode envelope on cg=${contextGraphId} from=${from}: ` +
558
- `private-ciphertext strip is ON for a curated CG (OT-RFC-49 WS-A)`);
559
- return;
560
- }
561
- // OT-RFC-38 LU-11: peek envelope type and dispatch. Chunked
562
- // envelopes (`type='share-write-chunked'`) take the V2 chunk
563
- // persistence path; everything else flows through the legacy
564
- // host-mode store unchanged. Failed decode falls through to
565
- // `ingestSwmHostModeEnvelope` which is also defensive — the
566
- // dispatch here is best-effort, not a security boundary.
567
- let envelopeType;
568
- try {
569
- const peek = decodeGossipEnvelope(data);
570
- envelopeType = peek?.type;
571
- }
572
- catch { /* drop into legacy path */ }
573
- if (envelopeType === GOSSIP_TYPE_WORKSPACE_PUBLISH_CHUNKED) {
574
- this.ingestSwmCiphertextChunkEnvelope(contextGraphId, data, from).catch((err) => {
575
- const msg = err instanceof Error ? err.message : String(err);
576
- this.log.warn(createOperationContext('system'), `LU-11: chunked SWM ingest failed for "${contextGraphId}": ${msg}`);
577
- });
578
- return;
579
- }
580
- this.ingestSwmHostModeEnvelope(contextGraphId, data, from).catch((err) => {
581
- const msg = err instanceof Error ? err.message : String(err);
582
- this.log.warn(createOperationContext('system'), `Host-mode SWM ingest failed for "${contextGraphId}": ${msg}`);
583
- });
584
- };
585
- this.swmHostModeHandlers.set(wireCgId, handler);
586
- this.gossip.onMessage(swmTopic, handler);
559
+ session.swmHostModeSubscribed.set(wireCgId, source);
560
+ session.swmHostModeCurated.set(wireCgId, curated);
561
+ live.manager.subscribe(swmTopic);
562
+ const handler = createSwmHostModeHandler(this, this.log, session, contextGraphId, wireCgId);
563
+ session.swmHostModeHandlers.set(wireCgId, handler);
564
+ live.manager.onMessage(swmTopic, handler);
587
565
  // B3: persist the host-mode designation so a restart re-engages
588
566
  // this handler before the chain-event poller catches up.
589
567
  // Codex PR #620 R2: chain wire/unwire writes through a per-CG
@@ -1622,7 +1600,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
1622
1600
  }
1623
1601
  if (!authOk) {
1624
1602
  try {
1625
- const allowedPeers = await this.getContextGraphAllowedPeers(req.contextGraphId);
1603
+ const allowedPeers = await this.resolveSwmAllowedPeersForCurrentAuthority(req.contextGraphId);
1626
1604
  if (allowedPeers !== null) {
1627
1605
  anyAuthorityFound = true;
1628
1606
  if (allowedPeers.includes(fromPeerId))
@@ -1931,6 +1909,20 @@ export class SwmHostModeMethods extends DKGAgentBase {
1931
1909
  `${transition.previous?.onChainId}->${newOnChainId}; reset reconcile watermark + cursor to 0`);
1932
1910
  }
1933
1911
  }
1912
+ /**
1913
+ * Inverse of {@link bindSubscriptionOnChainId}: drop a local CG's on-chain
1914
+ * id together with every piece of reconcile progress made against it. The
1915
+ * persisted watermark and the in-memory cursor count ordinals of the graph
1916
+ * that id named, so neither may survive the id. The canonical setter
1917
+ * persists the unbound row.
1918
+ */
1919
+ unbindSubscriptionOnChainId(localCgId) {
1920
+ const sub = this.subscribedContextGraphs.get(localCgId);
1921
+ if (sub === undefined)
1922
+ return;
1923
+ this.forceClearVmReconcileStateForContextGraph(localCgId);
1924
+ this.setContextGraphSubscription(localCgId, { ...sub, onChainId: undefined, lastReconciledOrdinal: 0 });
1925
+ }
1934
1926
  /**
1935
1927
  * Install a reverse-derived VM candidate without promoting it to the shared
1936
1928
  * authoritative `onChainId` field. The candidate is process-local and every
@@ -2370,7 +2362,9 @@ export class SwmHostModeMethods extends DKGAgentBase {
2370
2362
  return null;
2371
2363
  if (resolved.provenance !== 'reverse-name-hash') {
2372
2364
  try {
2373
- await this.persistContextGraphSubscriptionStrict(localCgId, { ...sub, onChainId: resolved.onChainId }, undefined, isSubscriptionCurrent);
2365
+ // An on-demand subscription binds in memory only; durable rows are
2366
+ // saved before the binding becomes visible.
2367
+ await this.persistContextGraphSyncStateStrict(localCgId, { ...sub, onChainId: resolved.onChainId }, 'on-chain id binding', isSubscriptionCurrent);
2374
2368
  }
2375
2369
  catch {
2376
2370
  return null;
@@ -2464,6 +2458,534 @@ export class SwmHostModeMethods extends DKGAgentBase {
2464
2458
  }
2465
2459
  return localCgId;
2466
2460
  }
2461
+ /**
2462
+ * #2858 — body of the live `KnowledgeAssetUpdated` nudge.
2463
+ *
2464
+ * An update keeps the KA id and moves its latest root. The ordinal sweep
2465
+ * never revisits a settled ordinal, and a node whose only copy is a
2466
+ * confirmed VM copy (a member of a catalog graph, a core outside the update's
2467
+ * ACK set) has no other lane that brings the new version. This decides from
2468
+ * local state alone, with no chain read, for every VM-reconcile target graph
2469
+ * that holds a confirmed copy of the KA; a queued target gets a reconcile
2470
+ * pass, whose refresh worker reads the chain and fetches the current version
2471
+ * ({@link startVmRefreshWorker}).
2472
+ *
2473
+ * - A copy at another root is queued for a refresh.
2474
+ * - A copy already at the event's root is skipped when it reflects the
2475
+ * update: the event's own transaction confirmed it (the publisher), or it
2476
+ * was materialized from a chain view at or after the event's block (a
2477
+ * replayed event). Otherwise it may be an older version with the same
2478
+ * content, so a version check is queued.
2479
+ * - When the workspace already stages a newer version than the confirmed
2480
+ * copy (the publisher mid-update, a core holding the update's ACK copy, a
2481
+ * member that recovered its curator's shared memory), the target waits
2482
+ * `VM_REFRESH_STAGED_GRACE_MS` for the lane that staged it; on a member no
2483
+ * lane promotes it, and the refresh does.
2484
+ *
2485
+ * The event's root is only a trigger: an older or superseded event is
2486
+ * settled by the worker's chain read, never materialized. Every no-op is
2487
+ * logged at debug. A store read that fails, or a full target set, throws, so
2488
+ * the event lane holds the event and dispatches it again after its failure
2489
+ * backoff instead of losing it.
2490
+ *
2491
+ * Returns the refresh targets newly queued.
2492
+ */
2493
+ async handleKAUpdatedNudge(kaId, merkleRoot, ctx, options = {}) {
2494
+ const { signal } = options;
2495
+ const blockNumber = options.blockNumber !== undefined
2496
+ && Number.isSafeInteger(options.blockNumber)
2497
+ && options.blockNumber >= 0
2498
+ ? options.blockNumber
2499
+ : undefined;
2500
+ const eventTxHash = typeof options.txHash === 'string' && options.txHash.length > 0
2501
+ ? options.txHash.toLowerCase()
2502
+ : undefined;
2503
+ const logIndex = options.logIndex !== undefined
2504
+ && Number.isSafeInteger(options.logIndex)
2505
+ && options.logIndex >= 0
2506
+ ? options.logIndex
2507
+ : undefined;
2508
+ const blockHash = typeof options.blockHash === 'string' && options.blockHash.length > 0
2509
+ ? options.blockHash.toLowerCase()
2510
+ : undefined;
2511
+ signal?.throwIfAborted();
2512
+ const lifecycleGeneration = this.vmReconcileLifecycleGeneration;
2513
+ const lifecycleSignal = this.vmReconcileLifecycleController?.signal;
2514
+ const isLifecycleCurrent = () => !this.vmReconcileRotationClosed
2515
+ && !signal?.aborted
2516
+ && !lifecycleSignal?.aborted
2517
+ && this.vmReconcileLifecycleGeneration === lifecycleGeneration;
2518
+ if (!isLifecycleCurrent() || kaId <= 0n || merkleRoot.length !== 32)
2519
+ return [];
2520
+ this.expireVmRefreshTargets(ctx);
2521
+ const update = `update${blockNumber === undefined ? '' : ` at block ${blockNumber}`}`;
2522
+ let ual;
2523
+ let holders;
2524
+ try {
2525
+ const storageAddr = this.chain.getDKGKnowledgeAssetsAddress
2526
+ ? await this.chain.getDKGKnowledgeAssetsAddress()
2527
+ : undefined;
2528
+ if (!storageAddr)
2529
+ return [];
2530
+ ual = buildReconciledKnowledgeAssetUal(this.chain.chainId, storageAddr, kaId);
2531
+ holders = await this.localGraphsDescribingKnowledgeAsset(ual);
2532
+ }
2533
+ catch (err) {
2534
+ throw new Error(`VM refresh: could not read which local graphs hold KA ${kaId} for its ${update}: `
2535
+ + `${err instanceof Error ? err.message : String(err)}`, { cause: err });
2536
+ }
2537
+ const eventRoot = ethers.hexlify(merkleRoot).toLowerCase();
2538
+ const queued = [];
2539
+ if (holders.length === 0) {
2540
+ this.log.debug(ctx, `VM refresh: no local graph holds ${ual}; nothing to refresh`);
2541
+ }
2542
+ for (const localCgId of holders) {
2543
+ if (!isLifecycleCurrent())
2544
+ break;
2545
+ if (!this.isVmReconcileTargetSelected(localCgId)) {
2546
+ this.log.debug(ctx, `VM refresh: "${localCgId}" holds ${ual} but is not a VM reconcile target; not queued`);
2547
+ continue;
2548
+ }
2549
+ let local;
2550
+ let coversUpdate = false;
2551
+ try {
2552
+ local = await this.readVmRefreshLocalState(localCgId, ual);
2553
+ if (local.kind === 'confirmed' && local.merkleRoot === eventRoot) {
2554
+ coversUpdate = await this.vmRefreshCopyCoversUpdate(localCgId, ual, local.transactionHash, eventTxHash, blockNumber, blockHash);
2555
+ }
2556
+ }
2557
+ catch (err) {
2558
+ throw new Error(`VM refresh: could not read the copy of ${ual} in "${localCgId}" for its ${update}: `
2559
+ + `${err instanceof Error ? err.message : String(err)}`, { cause: err });
2560
+ }
2561
+ if (local.kind !== 'confirmed') {
2562
+ this.log.debug(ctx, `VM refresh: ${ual} in "${localCgId}" not queued: no confirmed copy`);
2563
+ continue;
2564
+ }
2565
+ const sameRoot = local.merkleRoot === eventRoot;
2566
+ if (coversUpdate) {
2567
+ this.log.debug(ctx, `VM refresh: ${ual} in "${localCgId}" not queued: the copy holds this update`);
2568
+ continue;
2569
+ }
2570
+ if (!isLifecycleCurrent())
2571
+ break;
2572
+ const target = {
2573
+ localCgId,
2574
+ ual,
2575
+ kaId,
2576
+ merkleRoot: eventRoot,
2577
+ ...(blockNumber === undefined ? {} : { blockNumber }),
2578
+ ...(logIndex === undefined ? {} : { logIndex }),
2579
+ ...(blockHash === undefined ? {} : { blockHash }),
2580
+ ...(eventTxHash === undefined ? {} : { txHash: eventTxHash }),
2581
+ ...(sameRoot ? { checkVersion: true } : {}),
2582
+ };
2583
+ const delayMs = local.staged ? DKGAgentBase.VM_REFRESH_STAGED_GRACE_MS : 0;
2584
+ // A replayed event (a lane re-scan) leaves the held target and its
2585
+ // backoff alone.
2586
+ const offered = this.vmRefreshQueue.offer(target, delayMs);
2587
+ if (offered === 'held') {
2588
+ this.log.debug(ctx, `VM refresh: ${ual} in "${localCgId}" already has this or a newer update queued`);
2589
+ continue;
2590
+ }
2591
+ if (offered === 'full') {
2592
+ throw new Error(`VM refresh: ${this.vmRefreshQueue.size} targets are held, the most allowed, so the `
2593
+ + `${update} of ${ual} in "${localCgId}" waits for some to settle`);
2594
+ }
2595
+ queued.push(target);
2596
+ if (sameRoot) {
2597
+ this.log.debug(ctx, `VM refresh: ${ual} in "${localCgId}" holds its update's root; version check queued`);
2598
+ }
2599
+ else if (local.staged) {
2600
+ this.log.info(ctx, `VM refresh: ${ual} in "${localCgId}" does not hold its update's root and stages a newer `
2601
+ + `version; refresh queued in ${Math.round(delayMs / 1000)}s unless that version is promoted first`);
2602
+ }
2603
+ else {
2604
+ this.log.info(ctx, `VM refresh: ${ual} in "${localCgId}" does not hold its update's root; refresh queued`);
2605
+ }
2606
+ // A delayed target is not due yet: the graph's next reconcile pass
2607
+ // starts the worker.
2608
+ if (delayMs === 0 && this.vmReconcileScheduling && isLifecycleCurrent()) {
2609
+ this.vmReconcileScheduling.triggerLive(localCgId);
2610
+ }
2611
+ }
2612
+ return queued;
2613
+ }
2614
+ /**
2615
+ * #2858 — whether a copy at an update's root already reflects that update:
2616
+ * the update's own transaction confirmed it, or it was materialized from a
2617
+ * chain view at or after the update's block, which already held the update.
2618
+ * Neither fact proves fork lineage when the event carries a block hash,
2619
+ * because the same transaction can be included on a replacement fork.
2620
+ */
2621
+ async vmRefreshCopyCoversUpdate(localCgId, ual, copyTxHash, eventTxHash, eventBlock, eventBlockHash) {
2622
+ if (eventBlockHash !== undefined)
2623
+ return false;
2624
+ if (eventTxHash !== undefined && copyTxHash === eventTxHash)
2625
+ return true;
2626
+ if (eventBlock === undefined)
2627
+ return false;
2628
+ const materialized = await readMaterializedVersion(this.store, contextGraphMetaUri(localCgId), ual, { source: 'agent.vmRefresh.materializedVersion', priority: 'background' });
2629
+ return materialized !== null && materialized.blockNumber >= eventBlock;
2630
+ }
2631
+ /** #2858 — give up the refresh targets held past `VM_REFRESH_MAX_AGE_MS`, with a warning each. */
2632
+ expireVmRefreshTargets(ctx) {
2633
+ // Narrow test agents built without the base constructor have no queue.
2634
+ const queue = this.vmRefreshQueue;
2635
+ if (queue === undefined)
2636
+ return;
2637
+ for (const given of queue.expire()) {
2638
+ this.log.warn(ctx, `VM refresh: gave up on ${given.ual} in "${given.localCgId}" after `
2639
+ + `${Math.round(given.heldMs / 60_000)} min and ${given.failures} failed attempt(s) `
2640
+ + `(${queue.givenUpTotal} given up so far); its copy waits for the KA's next update `
2641
+ + 'or an asset fetch');
2642
+ }
2643
+ }
2644
+ /**
2645
+ * #2858 — highest block the update event lane may persist as scanned: just
2646
+ * below the oldest held target's event, so a restart replays that event and
2647
+ * its target is recorded again. Targets held past their maximum age are
2648
+ * given up first, so one that never settles cannot hold the cursor for good.
2649
+ */
2650
+ vmRefreshPersistCeiling() {
2651
+ this.expireVmRefreshTargets(createOperationContext('system'));
2652
+ const oldest = this.vmRefreshQueue?.oldestBlockNumber();
2653
+ return oldest === undefined ? undefined : oldest - 1;
2654
+ }
2655
+ /** #2858 — the refresh lane's state, for `/api/status`. */
2656
+ getVmRefreshStatus() {
2657
+ const queue = this.vmRefreshQueue;
2658
+ const oldest = queue?.oldestBlockNumber();
2659
+ return {
2660
+ heldTargets: queue?.size ?? 0,
2661
+ persistCeiling: oldest === undefined ? null : oldest - 1,
2662
+ givenUpTotal: queue?.givenUpTotal ?? 0,
2663
+ refusedTotal: queue?.refusedTotal ?? 0,
2664
+ };
2665
+ }
2666
+ /**
2667
+ * Local graphs whose root metadata graph describes this graph-scoped KA.
2668
+ * One subject-bound read; the caller filters them to reconcile targets.
2669
+ */
2670
+ async localGraphsDescribingKnowledgeAsset(ual) {
2671
+ const result = await this.store.query(`SELECT DISTINCT ?g WHERE { GRAPH ?g { <${assertSafeIri(ual)}> `
2672
+ + `<${GRAPH_KA_CONTENT_SCOPE_VERSION_PREDICATE}> ?version } }`, { source: 'agent.vmRefresh.holders', priority: 'background' });
2673
+ if (result.type !== 'bindings')
2674
+ return [];
2675
+ const prefix = 'did:dkg:context-graph:';
2676
+ const suffix = '/_meta';
2677
+ const holders = new Set();
2678
+ for (const row of result.bindings) {
2679
+ const graph = row['g'];
2680
+ if (typeof graph !== 'string' || !graph.startsWith(prefix) || !graph.endsWith(suffix)) {
2681
+ continue;
2682
+ }
2683
+ const localCgId = graph.slice(prefix.length, graph.length - suffix.length);
2684
+ if (localCgId.length > 0 && contextGraphMetaUri(localCgId) === graph)
2685
+ holders.add(localCgId);
2686
+ }
2687
+ return [...holders];
2688
+ }
2689
+ /**
2690
+ * What the refresh lane decides on for one KA in one graph: the root and
2691
+ * assertion version the confirmed graph-scoped copy recorded, the
2692
+ * transaction that confirmed it when it is receipt-backed, and whether the
2693
+ * workspace stages a newer version than that copy. A staged version only
2694
+ * delays the refresh (see `VM_REFRESH_STAGED_GRACE_MS`). A head at the
2695
+ * confirmed version or older stages nothing newer; a corrupt one is read
2696
+ * past by the exact fetch.
2697
+ */
2698
+ async readVmRefreshLocalState(localCgId, ual) {
2699
+ const read = await readConfirmedGraphKnowledgeAssetMetadataEnvelope(this.store, {
2700
+ contextGraphId: localCgId,
2701
+ ual,
2702
+ });
2703
+ if (read.state !== 'confirmed')
2704
+ return { kind: 'none' };
2705
+ let staged = false;
2706
+ try {
2707
+ const head = await resolveKnowledgeAssetWorkspaceHead({
2708
+ store: this.store,
2709
+ graphManager: new GraphManager(this.store),
2710
+ contextGraphId: localCgId,
2711
+ kaUal: ual,
2712
+ ...(read.envelope.subGraphName ? { subGraphName: read.envelope.subGraphName } : {}),
2713
+ });
2714
+ staged = head !== undefined
2715
+ && BigInt(head.assertionVersion) > BigInt(read.envelope.assertionVersion);
2716
+ }
2717
+ catch (err) {
2718
+ if (!isKnowledgeAssetWorkspaceHeadCorruptError(err))
2719
+ throw err;
2720
+ }
2721
+ const transactionHash = read.envelope.transactionHash?.trim().toLowerCase();
2722
+ return {
2723
+ kind: 'confirmed',
2724
+ merkleRoot: ethers.hexlify(read.envelope.merkleRoot).toLowerCase(),
2725
+ assertionVersion: BigInt(read.envelope.assertionVersion),
2726
+ ...(transactionHash ? { transactionHash } : {}),
2727
+ staged,
2728
+ };
2729
+ }
2730
+ /**
2731
+ * #2858 — start this graph's refresh worker when it has due targets and
2732
+ * none is running. The worker runs beside the reconcile pass that starts
2733
+ * it, in the background RPC class and store lane, so the pass never waits
2734
+ * on a peer for a refresh. At most `VM_REFRESH_MAX_WORKERS` run at once; a
2735
+ * graph left waiting starts on its next pass or when a running worker ends.
2736
+ * Returns the graph's running worker, if any.
2737
+ */
2738
+ startVmRefreshWorker(localCgId, onChainId, isTargetCurrent, signal) {
2739
+ // Narrow test agents built without the base constructor have neither.
2740
+ const queue = this.vmRefreshQueue;
2741
+ const workers = this.vmRefreshWorkers;
2742
+ if (queue === undefined || workers === undefined)
2743
+ return undefined;
2744
+ const running = workers.get(localCgId);
2745
+ if (running !== undefined)
2746
+ return running;
2747
+ if (signal?.aborted || !isTargetCurrent() || !queue.hasDue(localCgId))
2748
+ return undefined;
2749
+ if (workers.size >= DKGAgentBase.VM_REFRESH_MAX_WORKERS) {
2750
+ this.log.debug(createOperationContext('system'), `VM refresh for "${localCgId}" waits for a free worker`);
2751
+ return undefined;
2752
+ }
2753
+ const worker = withOwnedRpcRequestContext({ requestClass: 'background', ...(signal ? { signal } : {}) }, () => withDefaultStoreWorkPriority('background', () => this.runVmRefreshesForCg(localCgId, onChainId, isTargetCurrent, signal))).catch((err) => {
2754
+ this.log.warn(createOperationContext('system'), `VM refresh worker for "${localCgId}" stopped: `
2755
+ + `${err instanceof Error ? err.message : String(err)}`);
2756
+ }).finally(() => {
2757
+ if (workers.get(localCgId) === worker)
2758
+ workers.delete(localCgId);
2759
+ if (signal?.aborted || this.vmReconcileRotationClosed)
2760
+ return;
2761
+ // Hand the freed slot on: this graph past its per-run bound, or a graph
2762
+ // that found every slot taken. Their passes start the workers.
2763
+ let free = DKGAgentBase.VM_REFRESH_MAX_WORKERS - workers.size;
2764
+ for (const dueCgId of queue.dueContextGraphIds()) {
2765
+ if (free <= 0)
2766
+ break;
2767
+ if (workers.has(dueCgId))
2768
+ continue;
2769
+ if (!this.isVmReconcileTargetSelected(dueCgId)) {
2770
+ // The graph left VM reconciliation; no pass will work its targets.
2771
+ queue.clearContextGraph(dueCgId);
2772
+ continue;
2773
+ }
2774
+ void this.vmReconcileScheduling?.triggerLive(dueCgId);
2775
+ free -= 1;
2776
+ }
2777
+ });
2778
+ workers.set(localCgId, worker);
2779
+ trackVmReconcilePhysicalRun(this.vmReconcilePhysicalRuns, worker);
2780
+ return worker;
2781
+ }
2782
+ /**
2783
+ * Work off up to `VM_REFRESH_MAX_PER_PASS` of this graph's due refresh
2784
+ * targets. Each attempt is bounded by `VM_REFRESH_ATTEMPT_TIMEOUT_MS` and
2785
+ * its peer steps by `VM_REFRESH_PEER_STEP_TIMEOUT_MS`, and every outcome is
2786
+ * settled and logged: an attempt that fails, times out or finds no holder
2787
+ * backs off and asks the next peer window when it is due again. A closing
2788
+ * lifecycle or a replaced reconcile target keeps the target for the next
2789
+ * worker. Never throws.
2790
+ */
2791
+ async runVmRefreshesForCg(localCgId, onChainId, isTargetCurrent, signal) {
2792
+ // Narrow test agents built without the base constructor have no queue.
2793
+ const queue = this.vmRefreshQueue;
2794
+ if (queue === undefined)
2795
+ return;
2796
+ const due = queue.due(localCgId, DKGAgentBase.VM_REFRESH_MAX_PER_PASS);
2797
+ if (due.length === 0)
2798
+ return;
2799
+ const ctx = createOperationContext('system');
2800
+ const stopped = (target) => {
2801
+ if (!signal?.aborted && isTargetCurrent())
2802
+ return false;
2803
+ this.log.debug(ctx, `VM refresh of ${target.ual} in "${localCgId}" deferred: its reconcile target closed`);
2804
+ return true;
2805
+ };
2806
+ for (const target of due) {
2807
+ if (stopped(target))
2808
+ return;
2809
+ let attempt;
2810
+ let failed = false;
2811
+ try {
2812
+ attempt = await runBoundedOperation((attemptSignal) => this.refreshConfirmedVmCopy(target, onChainId, isTargetCurrent, attemptSignal), {
2813
+ timeoutMs: DKGAgentBase.VM_REFRESH_ATTEMPT_TIMEOUT_MS,
2814
+ label: `VM refresh of ${target.ual}`,
2815
+ ...(signal ? { signal } : {}),
2816
+ });
2817
+ }
2818
+ catch (err) {
2819
+ if (stopped(target))
2820
+ return;
2821
+ failed = true;
2822
+ attempt = {
2823
+ outcome: 'retry',
2824
+ detail: isBoundedOperationTimeoutError(err)
2825
+ ? err.message
2826
+ : `failed: ${err instanceof Error ? err.message : String(err)}`,
2827
+ };
2828
+ }
2829
+ if (stopped(target))
2830
+ return;
2831
+ const retryAt = queue.settle(target, attempt.outcome);
2832
+ const subject = `${target.ual} in "${localCgId}"`;
2833
+ if (attempt.outcome === 'refreshed') {
2834
+ this.log.info(ctx, `VM refresh: ${subject} now holds the current version (${attempt.detail})`);
2835
+ }
2836
+ else if (attempt.outcome === 'retry') {
2837
+ const retry = retryAt === undefined
2838
+ ? 'retrying on a later pass'
2839
+ : `retrying in ${Math.max(0, Math.round((retryAt - Date.now()) / 1000))}s`;
2840
+ const message = `VM refresh of ${subject} did not complete (${attempt.detail}); ${retry}`;
2841
+ if (failed)
2842
+ this.log.warn(ctx, message);
2843
+ else
2844
+ this.log.info(ctx, message);
2845
+ }
2846
+ else {
2847
+ this.log.debug(ctx, `VM refresh of ${subject} settled as ${attempt.outcome}: ${attempt.detail}`);
2848
+ }
2849
+ }
2850
+ }
2851
+ /**
2852
+ * One refresh attempt: settle from local state when the copy already holds
2853
+ * the event's root, otherwise read the chain's current root. A copy at that
2854
+ * root is current (the event was older or superseded — nothing is rolled
2855
+ * back); a different one is fetched as the exact current version, which
2856
+ * replaces the older assertion in place. `signal` is the attempt's own: it
2857
+ * stops the fetch at the attempt deadline.
2858
+ */
2859
+ async refreshConfirmedVmCopy(target, onChainId, isTargetCurrent, signal) {
2860
+ const local = await this.readVmRefreshLocalState(target.localCgId, target.ual);
2861
+ if (local.kind === 'none') {
2862
+ return { outcome: 'not-applicable', detail: 'no confirmed copy is held any more' };
2863
+ }
2864
+ // A version still staged here was not promoted by the lane that staged it
2865
+ // (or nothing on this node promotes it). The exact fetch below verifies it
2866
+ // against the chain like any other copy, and a staged version newer than
2867
+ // the chain's stays in the workspace: the fetch writes only Verifiable
2868
+ // Memory.
2869
+ // A copy at the update's root is current, unless the nudge found it
2870
+ // there already: then only the chain's version can tell (a repeated root).
2871
+ if (!target.checkVersion && local.merkleRoot === target.merkleRoot) {
2872
+ return { outcome: 'current', detail: 'the copy holds the update root' };
2873
+ }
2874
+ if (typeof this.chain.getLatestMerkleRoot !== 'function') {
2875
+ return { outcome: 'not-applicable', detail: 'the chain adapter reads no latest root' };
2876
+ }
2877
+ let chainRoot = ethers.hexlify(await this.chain.getLatestMerkleRoot(target.kaId, { signal })).toLowerCase();
2878
+ if (!isTargetCurrent())
2879
+ throw new VmReconcileQueueClosedError();
2880
+ let chainVersion;
2881
+ if (chainRoot === local.merkleRoot) {
2882
+ // An older or superseded event, a read that has not seen the update yet
2883
+ // (a lagging endpoint, a confirmation depth above one), or an update
2884
+ // that repeated the copy's root. Settle only on a coherent view at or
2885
+ // after the update's block, by version as well as root.
2886
+ const confirmed = await this.confirmVmRefreshCurrentAtEventBlock(target, local, isTargetCurrent, signal);
2887
+ if (confirmed.kind === 'settled')
2888
+ return confirmed.attempt;
2889
+ // The pinned view names a newer version: fetch it.
2890
+ chainRoot = confirmed.chainRoot;
2891
+ chainVersion = confirmed.rootCount;
2892
+ }
2893
+ let result;
2894
+ try {
2895
+ result = await this.runExactAssetFetchForContextGraph(target.localCgId, [target.ual], {
2896
+ isCurrent: () => isTargetCurrent() && !signal?.aborted,
2897
+ ...(signal ? { signal } : {}),
2898
+ expectedOnChainId: onChainId,
2899
+ maxPeers: DKGAgentBase.VM_RECONCILE_EXACT_PEER_MAX,
2900
+ // Each retry asks the next window of candidates, so a holder outside
2901
+ // the first few peers is still reached.
2902
+ peerWindowIndex: target.failures,
2903
+ // An update keeps the UAL. Its old responder session can outlive the
2904
+ // previous version, so every refresh attempt needs a new snapshot.
2905
+ forceFreshExactSession: true,
2906
+ // A candidate that cannot be reached (after a restart, the curator
2907
+ // is often not connected yet) yields to the next one.
2908
+ peerStepTimeoutMs: DKGAgentBase.VM_REFRESH_PEER_STEP_TIMEOUT_MS,
2909
+ // Evidence that has not seen the update would settle on the old version.
2910
+ ...((target.proofBlockNumber ?? target.blockNumber) === undefined
2911
+ ? {} : { minVersionBlock: target.proofBlockNumber ?? target.blockNumber }),
2912
+ });
2913
+ }
2914
+ catch (err) {
2915
+ // This adapter cannot prove an exact version at all. Anything else,
2916
+ // including a conflict such as a version snapshot the endpoints did
2917
+ // not agree on, is retried after backoff.
2918
+ if (err instanceof VmReconcileUnavailableError) {
2919
+ return { outcome: 'not-applicable', detail: 'this adapter cannot prove an exact version' };
2920
+ }
2921
+ if (err instanceof ExactAssetVersionBehindError) {
2922
+ return { outcome: 'retry', detail: err.message };
2923
+ }
2924
+ throw err;
2925
+ }
2926
+ const status = result.items[0]?.status;
2927
+ if (status === 'materialized' || status === 'fetched') {
2928
+ return { outcome: 'refreshed', detail: `${status} after ${result.peerAttempts} peer attempt(s)` };
2929
+ }
2930
+ if (status === 'already-present') {
2931
+ return { outcome: 'current', detail: 'the exact inspection found the current version' };
2932
+ }
2933
+ // The exact inspection can refuse a refreshed copy while a workspace head
2934
+ // names another version (an older one, or one staged ahead of the chain);
2935
+ // the confirmed copy itself is the answer here.
2936
+ const after = await this.readVmRefreshLocalState(target.localCgId, target.ual);
2937
+ if (after.kind === 'confirmed'
2938
+ && after.merkleRoot === chainRoot
2939
+ && (chainVersion === undefined || after.assertionVersion >= chainVersion)) {
2940
+ return { outcome: 'refreshed', detail: 'the confirmed copy holds the chain root' };
2941
+ }
2942
+ return {
2943
+ outcome: 'retry',
2944
+ detail: `no peer served the current version; ${result.peerAttempts} peer attempt(s)`,
2945
+ };
2946
+ }
2947
+ /**
2948
+ * #2858 — the copy holds the root a live chain read returned. That read can
2949
+ * predate the update (a lagging endpoint, a confirmation depth above one),
2950
+ * and a root names content, not a version: an update may repeat an earlier
2951
+ * root. So settle from a coherent view at or after the event's block:
2952
+ * `current` when it holds the copy's root at a version no newer than the
2953
+ * copy's, `retry` while it has not reached that block, and the root and
2954
+ * version it shows otherwise, which the caller then fetches. On an adapter
2955
+ * that reads no pinned view, the live read settles it.
2956
+ */
2957
+ async confirmVmRefreshCurrentAtEventBlock(target, local, isTargetCurrent, signal) {
2958
+ const current = {
2959
+ kind: 'settled',
2960
+ attempt: { outcome: 'current', detail: 'the copy holds the chain root and version' },
2961
+ };
2962
+ if (typeof this.chain.readKnowledgeAssetVersionSnapshot !== 'function')
2963
+ return current;
2964
+ const view = await this.chain.readKnowledgeAssetVersionSnapshot(target.kaId, { signal });
2965
+ if (!isTargetCurrent())
2966
+ throw new VmReconcileQueueClosedError();
2967
+ if (view === null) {
2968
+ return {
2969
+ kind: 'settled',
2970
+ attempt: { outcome: 'retry', detail: 'no coherent chain view confirms the copy' },
2971
+ };
2972
+ }
2973
+ const proofBlock = target.proofBlockNumber ?? target.blockNumber;
2974
+ if (proofBlock !== undefined && view.blockNumber < proofBlock) {
2975
+ return {
2976
+ kind: 'settled',
2977
+ attempt: {
2978
+ outcome: 'retry',
2979
+ detail: `the chain view at block ${view.blockNumber} is behind the update's block `
2980
+ + `${proofBlock}`,
2981
+ },
2982
+ };
2983
+ }
2984
+ const viewRoot = view.latestRoot.toLowerCase();
2985
+ return viewRoot === local.merkleRoot && view.rootCount <= local.assertionVersion
2986
+ ? current
2987
+ : { kind: 'newer', chainRoot: viewRoot, rootCount: view.rootCount };
2988
+ }
2467
2989
  /**
2468
2990
  * Canonical evidence-gated VM reconciliation operation for one CG.
2469
2991
  *
@@ -2529,6 +3051,31 @@ export class SwmHostModeMethods extends DKGAgentBase {
2529
3051
  throw new VmReconcileQueueClosedError();
2530
3052
  if (!canRead)
2531
3053
  throw new ContextGraphNotFoundError(localCgId);
3054
+ // Bound through the prototype, as narrow hosts that borrow this operator
3055
+ // method do not carry the shared tail.
3056
+ return SwmHostModeMethods.prototype.runExactAssetFetchForContextGraph.call(this, localCgId, requestedUals, {
3057
+ isCurrent,
3058
+ ...(signal ? { signal } : {}),
3059
+ ...(options.peerIds === undefined ? {} : { peerIds: options.peerIds }),
3060
+ ...(subscription.onChainId ? { expectedOnChainId: subscription.onChainId } : {}),
3061
+ });
3062
+ }
3063
+ /**
3064
+ * The exact-asset fetch once its caller has proved read authority and owns
3065
+ * the lifecycle: chain evidence, local inspection against the current root
3066
+ * and version, a bounded peer traversal, and re-inspection. Shared by the
3067
+ * operator fetch above and the VM refresh lane ({@link refreshConfirmedVmCopy}).
3068
+ */
3069
+ async runExactAssetFetchForContextGraph(localCgId, requestedUals, options) {
3070
+ const { isCurrent, signal, peerStepTimeoutMs } = options;
3071
+ /** One network step, under `peerStepTimeoutMs` when the caller set it. */
3072
+ const runPeerStep = (label, step) => (peerStepTimeoutMs === undefined
3073
+ ? step(signal)
3074
+ : runBoundedOperation(step, {
3075
+ timeoutMs: peerStepTimeoutMs,
3076
+ label,
3077
+ ...(signal ? { signal } : {}),
3078
+ }));
2532
3079
  if (typeof this.chain.getKAContextGraphId !== 'function'
2533
3080
  || typeof this.chain.readKnowledgeAssetVersionSnapshot !== 'function') {
2534
3081
  throw new VmReconcileUnavailableError();
@@ -2541,7 +3088,11 @@ export class SwmHostModeMethods extends DKGAgentBase {
2541
3088
  contextGraphId: localCgId,
2542
3089
  requestedUals,
2543
3090
  ...(options.peerIds === undefined ? {} : { peerIds: options.peerIds }),
2544
- ...(subscription.onChainId ? { expectedOnChainId: subscription.onChainId } : {}),
3091
+ ...(options.expectedOnChainId ? { expectedOnChainId: options.expectedOnChainId } : {}),
3092
+ ...(options.maxPeers === undefined ? {} : { maxPeers: options.maxPeers }),
3093
+ ...(options.minVersionBlock === undefined
3094
+ ? {}
3095
+ : { minVersionBlock: options.minVersionBlock }),
2545
3096
  }, {
2546
3097
  chainId: this.chain.chainId,
2547
3098
  signal,
@@ -2578,34 +3129,59 @@ export class SwmHostModeMethods extends DKGAgentBase {
2578
3129
  return 'missing';
2579
3130
  },
2580
3131
  resolvePeerIds: async () => {
2581
- const curatorResolution = await this.resolveCuratorPeerIdsForCg(localCgId, {
3132
+ const curatorResolution = await runPeerStep(`Curator resolution for "${localCgId}"`, (stepSignal) => this.resolveCuratorPeerIdsForCg(localCgId, {
2582
3133
  maxPeerIds: MAX_CONTEXT_GRAPH_ASSET_FETCH_PEERS,
2583
- signal,
3134
+ signal: stepSignal,
2584
3135
  isCurrent,
2585
- }).catch(() => ({ peerIds: [] }));
3136
+ })).catch((error) => {
3137
+ if (isBoundedOperationTimeoutError(error)) {
3138
+ this.log.info(ctx, `Exact asset fetch: ${error.message}; asking connected peers`);
3139
+ }
3140
+ return { peerIds: [] };
3141
+ });
2586
3142
  if (!isCurrent())
2587
3143
  throw new VmReconcileQueueClosedError();
2588
3144
  const connectedPeerIds = this.node?.libp2p?.getConnections?.()
2589
3145
  ?.map((connection) => connection.remotePeer.toString()) ?? [];
2590
- return [...new Set([
2591
- ...curatorResolution.peerIds,
2592
- this.preferredSyncPeers.get(localCgId),
2593
- ...connectedPeerIds,
2594
- ].filter((peerId) => Boolean(peerId && peerId !== this.peerId)))];
3146
+ const usable = (peerId) => Boolean(peerId && peerId !== this.peerId);
3147
+ const curators = [...new Set(curatorResolution.peerIds.filter(usable))];
3148
+ const curatorSet = new Set(curators);
3149
+ const ordinary = [...new Set([this.preferredSyncPeers.get(localCgId), ...connectedPeerIds].filter(usable))].filter((peerId) => !curatorSet.has(peerId));
3150
+ const windowIndex = options.peerWindowIndex !== undefined
3151
+ && Number.isSafeInteger(options.peerWindowIndex)
3152
+ && options.peerWindowIndex > 0
3153
+ ? options.peerWindowIndex
3154
+ : 0;
3155
+ const windowSize = exactAssetFetchPeerWindow(options.maxPeers);
3156
+ const curatorSlots = Math.min(curators.length, windowSize);
3157
+ return [
3158
+ ...rotatePeerIds(curators, windowIndex * curatorSlots),
3159
+ ...rotatePeerIds(ordinary, windowIndex * (windowSize - curatorSlots)),
3160
+ ];
2595
3161
  },
2596
- preparePeer: async (peerId) => {
2597
- await this.ensurePeerConnected(peerId, { signal });
3162
+ // A step past its bound rejects; the traversal logs it and moves on to
3163
+ // the next candidate.
3164
+ preparePeer: (peerId) => runPeerStep('Peer preparation', async (stepSignal) => {
3165
+ await this.ensurePeerConnected(peerId, { signal: stepSignal });
2598
3166
  if (!isCurrent())
2599
3167
  throw new VmReconcileQueueClosedError();
2600
3168
  const remotePeer = this.node.libp2p.getConnections()
2601
3169
  .find((connection) => connection.remotePeer.toString() === peerId)
2602
3170
  ?.remotePeer;
2603
- if (!remotePeer || !(await this.waitForSyncProtocol(remotePeer, signal)))
3171
+ if (!remotePeer || !(await this.waitForSyncProtocol(remotePeer, stepSignal)))
2604
3172
  return false;
2605
- return this.ensurePeerAdmittedForRecovery(peerId, ctx, 'Exact asset fetch peer', signal);
2606
- },
3173
+ return this.ensurePeerAdmittedForRecovery(peerId, ctx, 'Exact asset fetch peer', stepSignal);
3174
+ }),
2607
3175
  fetchFromPeer: async (peerId, uals) => {
2608
- await this.syncExactKnowledgeAssetsFromPeerDetailed(peerId, localCgId, [...uals], { signal, isCurrent });
3176
+ await this.syncExactKnowledgeAssetsFromPeerDetailed(peerId, localCgId, [...uals], {
3177
+ signal,
3178
+ isCurrent,
3179
+ // The same UAL can move to a new root while the responder still
3180
+ // holds its older exact-asset page session. A refresh must start
3181
+ // from a new snapshot on every attempt; its pages still share one
3182
+ // session within this invocation.
3183
+ forceFreshExactSession: options.forceFreshExactSession === true,
3184
+ });
2609
3185
  },
2610
3186
  flush: async () => {
2611
3187
  await this.store.flush?.({
@@ -2638,12 +3214,18 @@ export class SwmHostModeMethods extends DKGAgentBase {
2638
3214
  this.log.debug(createOperationContext('system'), `VM reconcile for "${localCgId}" stopped: superseded by cleartext adoption of "${supersedingId}"`);
2639
3215
  return;
2640
3216
  }
3217
+ // Reported where the graph was parked (executeVmReconcileForCg): it
3218
+ // is asked again shortly and does not wait for the sweep.
3219
+ if (err instanceof VmReconcileReadAuthorityUnansweredError)
3220
+ return;
2641
3221
  this.log.warn(createOperationContext('system'), `VM reconcile for "${localCgId}" failed; retrying on the periodic sweep: ${err instanceof Error ? err.message : String(err)}`);
2642
3222
  }, {
2643
3223
  concurrency: DKGAgentBase.VM_RECONCILE_CONCURRENCY,
2644
3224
  maxPending: DKGAgentBase.VM_RECONCILE_QUEUE_MAX_PENDING,
2645
3225
  maxForegroundBurst: DKGAgentBase.VM_RECONCILE_MAX_FOREGROUND_BURST,
2646
- }, DKGAgentBase.VM_RECONCILE_UNBOUND_BATCH_SIZE);
3226
+ discoveryBatchSize: DKGAgentBase.VM_RECONCILE_UNBOUND_BATCH_SIZE,
3227
+ periodicBoundBatchSize: DKGAgentBase.VM_RECONCILE_PERIODIC_BOUND_BATCH_SIZE,
3228
+ });
2647
3229
  this.vmReconcileScheduling = scheduling;
2648
3230
  }
2649
3231
  return scheduling;
@@ -2668,11 +3250,46 @@ export class SwmHostModeMethods extends DKGAgentBase {
2668
3250
  ? work()
2669
3251
  : withOwnedRpcRequestContext({ requestClass: 'background', ...(lifecycleSignal ? { signal: lifecycleSignal } : {}) }, () => withDefaultStoreWorkPriority('background', work));
2670
3252
  const physicalRun = runInLane(async () => {
2671
- const target = await this.resolveVmReconcileTarget(localCgId, isLifecycleCurrent, lifecycleSignal);
3253
+ const passStartedAt = performance.now();
3254
+ const passGapMs = vmReconcilePassGapMs(this, localCgId, passStartedAt);
3255
+ const target = await this.resolveVmReconcileTarget(localCgId, isLifecycleCurrent, lifecycleSignal).catch((err) => {
3256
+ if (!(err instanceof VmReconcileReadAuthorityUnansweredError))
3257
+ throw err;
3258
+ // The chain read behind the read-authority check got no answer, which
3259
+ // is the node's RPC budget or its endpoint and not the graph. An
3260
+ // automatic pass leaves the graph waiting to be asked again; without
3261
+ // that its next attempt is the periodic sweep, up to a full interval
3262
+ // after the read would have been answered. It is asked again only
3263
+ // while its fetch could start, the readiness every waiter uses. An
3264
+ // operator's request, and a graph the wait cannot take, end as they
3265
+ // always did.
3266
+ const waiting = source === 'manual'
3267
+ ? undefined
3268
+ : this.vmReconcileScheduling?.deferForReadAuthority(localCgId, {
3269
+ signal: lifecycleSignal,
3270
+ isCurrent: isLifecycleCurrent,
3271
+ canAdmit: () => this.vmRecoverySyncAdmissionAvailable(localCgId),
3272
+ });
3273
+ if (waiting === undefined)
3274
+ throw new ContextGraphNotFoundError(localCgId);
3275
+ const message = `VM reconcile for "${localCgId}" is waiting for read authority `
3276
+ + `(${err.readAuthority}): the chain read got no answer; asking again shortly`;
3277
+ if (waiting === 'parked')
3278
+ this.log.info(createOperationContext('system'), message);
3279
+ else
3280
+ this.log.debug(createOperationContext('system'), message);
3281
+ throw err;
3282
+ });
3283
+ const resolveTargetMs = performance.now() - passStartedAt;
2672
3284
  const isTargetCurrent = () => isLifecycleCurrent()
2673
3285
  && this.isVmReconcileTargetCurrent(localCgId, target, lifecycleGeneration);
2674
3286
  if (!isTargetCurrent())
2675
3287
  throw new VmReconcileQueueClosedError();
3288
+ // #2858 — confirmed copies behind an on-chain update. The ordinal cursor
3289
+ // never revisits a settled ordinal, so a worker beside this pass
3290
+ // refreshes them. The pass never waits on it, and a slice that fails
3291
+ // does not hold it back.
3292
+ this.startVmRefreshWorker(localCgId, target.onChainId, isTargetCurrent, lifecycleSignal);
2676
3293
  // Reconcile on a private cursor snapshot. The caller-facing abort race may
2677
3294
  // finish before an adapter physically settles; a stale continuation must
2678
3295
  // never mutate the live cursor or persist a watermark into a new binding.
@@ -2683,10 +3300,12 @@ export class SwmHostModeMethods extends DKGAgentBase {
2683
3300
  };
2684
3301
  let pendingWatermark;
2685
3302
  let result;
3303
+ let engineTimings;
2686
3304
  try {
2687
3305
  result = await reconcileContextGraph(this.createVmReconcileDeps(localCgId, lifecycleGeneration, target, lifecycleSignal, {
2688
3306
  identityCursor: target.cursor,
2689
3307
  persistWatermark: (_lcg, watermark) => { pendingWatermark = watermark; },
3308
+ observePassTimings: (timings) => { engineTimings = timings; },
2690
3309
  }), workingCursor, localCgId, target.onChainCgId);
2691
3310
  }
2692
3311
  catch (err) {
@@ -2706,16 +3325,22 @@ export class SwmHostModeMethods extends DKGAgentBase {
2706
3325
  }
2707
3326
  if (!isTargetCurrent())
2708
3327
  throw new VmReconcileQueueClosedError();
3328
+ let flushMs = 0;
2709
3329
  if (result.reconciled > 0 || pendingWatermark !== undefined) {
3330
+ const flushStartedAt = performance.now();
2710
3331
  await this.store.flush?.({
2711
3332
  priority: 'background',
2712
3333
  source: 'agent.vmReconcile.materialization.flush',
2713
3334
  });
3335
+ flushMs = performance.now() - flushStartedAt;
2714
3336
  if (!isTargetCurrent())
2715
3337
  throw new VmReconcileQueueClosedError();
2716
3338
  }
3339
+ let watermarkMs = 0;
2717
3340
  if (pendingWatermark !== undefined) {
3341
+ const watermarkStartedAt = performance.now();
2718
3342
  await this.persistVmReconcileWatermark(localCgId, pendingWatermark, target);
3343
+ watermarkMs = performance.now() - watermarkStartedAt;
2719
3344
  if (!isTargetCurrent())
2720
3345
  throw new VmReconcileQueueClosedError();
2721
3346
  }
@@ -2724,11 +3349,40 @@ export class SwmHostModeMethods extends DKGAgentBase {
2724
3349
  target.cursor.scanOrdinal = workingCursor.scanOrdinal;
2725
3350
  const response = this.toContextGraphReconcileResult(localCgId, source, target, result);
2726
3351
  this.emitVmReconcileTelemetry(localCgId, target, result, response.status);
3352
+ observeVmRecoveryTiming(() => {
3353
+ const timings = engineTimings;
3354
+ this.log.info(createOperationContext('system'), `VM reconcile pass timing for "${localCgId}": source=${source} `
3355
+ + `gapSincePreviousPassMs=${passGapMs === undefined ? '-' : Math.round(passGapMs)} `
3356
+ + `resolveTargetMs=${Math.round(resolveTargetMs)} `
3357
+ + `headReadMs=${Math.round(timings?.headReadMs ?? 0)} blockReadMs=${Math.round(timings?.blockReadMs ?? 0)} `
3358
+ + `scanMs=${Math.round(timings?.scanMs ?? 0)} recoverMs=${Math.round(timings?.recoverMs ?? 0)} `
3359
+ + `engineMs=${Math.round(timings?.totalMs ?? 0)} flushMs=${Math.round(flushMs)} `
3360
+ + `watermarkMs=${Math.round(watermarkMs)} totalMs=${Math.round(performance.now() - passStartedAt)} `
3361
+ + `processed=${result.processed} reconciled=${result.reconciled} pending=${result.pending} `
3362
+ + `continue=${result.shouldContinueImmediately ? 1 : 0}`);
3363
+ });
2727
3364
  // The reconciler owns the continuation policy: productive slices, stale
2728
3365
  // bindings, and explicit provider rotations continue immediately, while
2729
3366
  // pending-only historical inventory yields to the periodic sweep.
2730
- if (isLifecycleCurrent() && result.shouldContinueImmediately) {
2731
- this.vmReconcileScheduling?.triggerLive(localCgId);
3367
+ const localAdmissionWait = {
3368
+ signal: lifecycleSignal,
3369
+ isCurrent: isTargetCurrent,
3370
+ canAdmit: () => this.vmRecoverySyncAdmissionAvailable(localCgId),
3371
+ };
3372
+ if (isTargetCurrent() && result.localAdmissionDeferred) {
3373
+ this.vmReconcileScheduling?.retryLocalAdmission(localCgId, localAdmissionWait);
3374
+ }
3375
+ else if (isLifecycleCurrent() && result.shouldContinueImmediately) {
3376
+ // A graph that just fetched from peers takes its next turn behind the
3377
+ // graphs the node's sync admission refused while it did. Without that
3378
+ // the freed capacity goes back to whichever pass asks first, which is
3379
+ // this one, and the others repeat their passes to be refused again.
3380
+ const yielded = result.recoveryAttempted === true
3381
+ && !result.staleTarget
3382
+ && isTargetCurrent()
3383
+ && this.vmReconcileScheduling?.yieldLocalAdmissionTurn(localCgId, localAdmissionWait) === true;
3384
+ if (!yielded)
3385
+ this.vmReconcileScheduling?.triggerLive(localCgId);
2732
3386
  }
2733
3387
  else if (isTargetCurrent()) {
2734
3388
  // RS heal is bounded, best-effort maintenance. Run it only after the
@@ -2749,6 +3403,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
2749
3403
  if (!isTargetCurrent())
2750
3404
  throw new VmReconcileQueueClosedError();
2751
3405
  }
3406
+ noteVmReconcilePassEnd(this, localCgId, performance.now());
2752
3407
  return response;
2753
3408
  });
2754
3409
  trackVmReconcilePhysicalRun(this.vmReconcilePhysicalRuns, physicalRun);
@@ -2835,8 +3490,10 @@ export class SwmHostModeMethods extends DKGAgentBase {
2835
3490
  // Central defense for periodic, live-chain, and manual reconciliation.
2836
3491
  // Every dispatcher entry point converges here and must independently prove
2837
3492
  // read authority. Never let a persisted subscription authorize itself.
3493
+ let authorityDecision;
2838
3494
  const authorityRead = withRpcUsageSite(CG_AUTH_RPC_SITES.vmReconcile, () => this.canReadContextGraph(localCgId, {
2839
3495
  allowSubscriptionFallback: false,
3496
+ onReadAuthorityDecision: (decision) => { authorityDecision = decision; },
2840
3497
  }));
2841
3498
  // Cancellation releases the dispatcher worker, but an underlying store/RPC
2842
3499
  // read may ignore it. Keep that physical dependency in the shutdown drain.
@@ -2844,8 +3501,16 @@ export class SwmHostModeMethods extends DKGAgentBase {
2844
3501
  const canRead = await raceVmReconcileAbort(authorityRead, signal).catch(() => false);
2845
3502
  if (!isCurrent())
2846
3503
  throw new VmReconcileQueueClosedError();
2847
- if (!canRead)
3504
+ if (!canRead) {
3505
+ // Still a refusal for this pass. The class only records that the chain
3506
+ // never answered, so scheduling can ask again instead of treating the
3507
+ // graph as unreadable until the next sweep.
3508
+ const decision = authorityDecision;
3509
+ if (isUnansweredVmReconcileReadAuthority(decision)) {
3510
+ throw new VmReconcileReadAuthorityUnansweredError(localCgId, decision);
3511
+ }
2848
3512
  throw new ContextGraphNotFoundError(localCgId);
3513
+ }
2849
3514
  if (this.contextGraphBindingState.currentBindingFor(localCgId, sub) === undefined
2850
3515
  && sub.subscribed) {
2851
3516
  await this.selfPrimeSubscriptionOnChainId(localCgId, sub, undefined, isCurrent, signal);
@@ -3136,7 +3801,6 @@ export class SwmHostModeMethods extends DKGAgentBase {
3136
3801
  reconcileOrdinal: (lcg, ocg, ordinal, headBlock, context) => this.reconcileChainOrdinal(lcg, ocg, ordinal, headBlock, {
3137
3802
  isTargetCurrent,
3138
3803
  revalidateTarget,
3139
- deferActiveFetch: true,
3140
3804
  rememberFinalizedEvidence: context === undefined
3141
3805
  || context.headOrdinal - ordinal <= DKGAgentBase.VM_RECONCILE_BATCH_SIZE,
3142
3806
  }),
@@ -3171,6 +3835,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
3171
3835
  },
3172
3836
  confirmationDepth: DKGAgentBase.VM_RECONCILE_CONFIRMATION_DEPTH,
3173
3837
  log: (msg) => this.log.info(createOperationContext('system'), msg),
3838
+ ...(execution?.observePassTimings ? { observePassTimings: execution.observePassTimings } : {}),
3174
3839
  };
3175
3840
  }
3176
3841
  async persistVmReconcileWatermark(localCgId, watermark, target) {
@@ -3217,7 +3882,11 @@ export class SwmHostModeMethods extends DKGAgentBase {
3217
3882
  });
3218
3883
  return;
3219
3884
  }
3220
- await this.persistContextGraphSubscriptionStrict(localCgId, { ...sub, lastReconciledOrdinal: watermark }, undefined, isTargetCurrent);
3885
+ // Authoritative progress follows the subscription's own lifetime. A durable
3886
+ // row (always-on member intent, or a Core's host-only obligation) is saved
3887
+ // before the live cursor moves. An on-demand subscription writes nothing:
3888
+ // like the subscription itself, its progress lives only in this process.
3889
+ await this.persistContextGraphSyncStateStrict(localCgId, { ...sub, lastReconciledOrdinal: watermark }, 'VM reconcile cursor', isTargetCurrent);
3221
3890
  if (!isTargetCurrent())
3222
3891
  return;
3223
3892
  sub.lastReconciledOrdinal = watermark;
@@ -3545,452 +4214,56 @@ export class SwmHostModeMethods extends DKGAgentBase {
3545
4214
  return { status: 'skipped', reason: 'failed' };
3546
4215
  }
3547
4216
  }
3548
- async collectVmReconcileSwmCandidateState(localCgId) {
3549
- const candidateNamespaces = await this.collectVmReconcileSwmCandidateNamespacesBestEffort(localCgId);
3550
- return {
3551
- candidateNamespaces: candidateNamespaces.namespaces,
3552
- swmGen: await this.readVmReconcileSwmGen(candidateNamespaces.namespaces),
3553
- peerTopology: await this.vmReconcilePeerTopology(localCgId),
3554
- cleanMissPeerIds: [],
3555
- };
4217
+ initializeVmReconcilePublicCoreTransportPreferencePolicy() {
4218
+ this.vmReconcilePublicCoreTransportPreferencePolicy ??= new VmRecoveryCoreTransportPreferencePolicy({
4219
+ now: () => this.vmReconcileRotationNow(),
4220
+ connectionKey: peerId => this.getSyncReconcilerConnectionKey(peerId),
4221
+ supportsCore: peerId => this.peerCapabilityRegistry.supportsCore(peerId),
4222
+ holderReuseEnabled: resolveExactBatchStreamEnabled,
4223
+ captureScope: (localCgId, candidatePeerIds) => typeof this.chain.deploymentId === 'string' ? {
4224
+ deploymentId: this.chain.deploymentId,
4225
+ lifecycleGeneration: this.vmReconcileLifecycleGeneration,
4226
+ bindingGeneration: this.contextGraphBindingState.capture(localCgId),
4227
+ selectedBindingGeneration: this.selectedVmReconcileCursors.get(localCgId)?.bindingGeneration,
4228
+ candidatePeerIds: [...candidatePeerIds],
4229
+ } : undefined,
4230
+ scopeIsCurrent: (localCgId, scope) => this.chain.deploymentId === scope.deploymentId
4231
+ && this.vmReconcileLifecycleGeneration === scope.lifecycleGeneration
4232
+ && !this.vmReconcileRotationClosed
4233
+ && this.contextGraphBindingState.capture(localCgId) === scope.bindingGeneration
4234
+ && this.selectedVmReconcileCursors.get(localCgId)?.bindingGeneration === scope.selectedBindingGeneration
4235
+ && this.vmReconcilePeerMembershipMatches(new Set(scope.candidatePeerIds), this.vmReconcileObservedCandidatePeerIds(localCgId)),
4236
+ }, {
4237
+ ttlMs: DKGAgentBase.VM_RECONCILE_PUBLIC_CORE_TRANSPORT_TTL_MS,
4238
+ maxEntries: DKGAgentBase.VM_RECONCILE_CG_STATE_MAX_ENTRIES,
4239
+ });
3556
4240
  }
3557
- vmReconcileRootSwmCandidateNamespaces(localCgId) {
3558
- return [{
3559
- metaGraph: contextGraphWorkspaceMetaGraphUri(localCgId),
3560
- dataGraph: contextGraphWorkspaceGraphUri(localCgId),
3561
- }];
4241
+ vmReconcileRotationNow() {
4242
+ return performance.now();
3562
4243
  }
3563
- async collectVmReconcileSwmCandidateNamespaces(localCgId) {
3564
- const graphManager = new GraphManager(this.store);
3565
- // `listSubGraphs` finds sub-graphs by their data or `_meta` graphs; a
3566
- // hosted-only core holds only `<cg>/<sub>/_shared_memory*` copies, which
3567
- // its StorageACK ledger names.
3568
- const subGraphs = [...new Set([
3569
- ...await graphManager.listSubGraphs(localCgId),
3570
- ...await this.storageAckLedgerNamespaceSubGraphs(localCgId),
3571
- ])];
3572
- const subGraphNamespaces = subGraphs
3573
- .map((sg) => ({
3574
- metaGraph: graphManager.sharedMemoryMetaUri(localCgId, sg),
3575
- dataGraph: graphManager.sharedMemoryUri(localCgId, sg),
3576
- }))
3577
- .sort((a, b) => `${a.metaGraph}\0${a.dataGraph}`.localeCompare(`${b.metaGraph}\0${b.dataGraph}`));
3578
- return [
3579
- ...this.vmReconcileRootSwmCandidateNamespaces(localCgId),
3580
- ...subGraphNamespaces,
3581
- ];
4244
+ vmReconcileRotationSlotKey(target) {
4245
+ return `${target.localCgId}\0${target.onChainCgId}\0${target.ordinal}`;
3582
4246
  }
3583
- async collectVmReconcileSwmCandidateNamespacesBestEffort(localCgId) {
3584
- try {
3585
- return { namespaces: await this.collectVmReconcileSwmCandidateNamespaces(localCgId), complete: true };
3586
- }
3587
- catch {
3588
- return { namespaces: this.vmReconcileRootSwmCandidateNamespaces(localCgId), complete: false };
3589
- }
4247
+ clearVmReconcileRotationStateForSlot(localCgId, onChainCgId, ordinal) {
4248
+ this.vmReconcileRotationState.delete(`${localCgId}\0${onChainCgId.toString()}\0${ordinal}`);
3590
4249
  }
3591
- vmReconcileSwmNamespaceKey(candidateNamespaces) {
3592
- return candidateNamespaces
3593
- .map((namespace) => `${namespace.metaGraph}\0${namespace.dataGraph}`)
3594
- .sort()
3595
- .join('\n');
4250
+ vmReconcileRotationFingerprint(target) {
4251
+ return `${target.ual}\0${target.merkleRoot.toLowerCase()}`;
3596
4252
  }
3597
- async vmReconcilePeerTopology(localCgId) {
3598
- try {
3599
- const preferredPeerId = await this.resolvePreferredSyncPeerId(localCgId);
3600
- const isPrivateContextGraph = await this.isPrivateContextGraph(localCgId);
3601
- const libp2p = this.node?.libp2p;
3602
- const getConnections = libp2p?.getConnections;
3603
- if (typeof getConnections !== 'function')
3604
- return UNREADABLE_VM_RECONCILE_PEER_TOPOLOGY;
3605
- const peerIds = [...new Map(getConnections.call(libp2p)
3606
- .map((connection) => [connection.remotePeer?.toString(), connection.remotePeer])
3607
- .filter((entry) => typeof entry[0] === 'string' && entry[0].length > 0 && !!entry[1])).keys()].sort();
3608
- const orderedPeers = this.selectCatchupPeers(peerIds.map((peerId) => ({ toString: () => peerId })), preferredPeerId, isPrivateContextGraph);
3609
- return createVmReconcilePeerTopology({
3610
- preferredPeerId: preferredPeerId ?? null,
3611
- privateOnly: isPrivateContextGraph,
3612
- peers: orderedPeers.map((peer) => {
3613
- const peerId = peer.toString();
3614
- return {
3615
- peerId,
3616
- core: this.peerCapabilityRegistry.supportsCore(peerId),
3617
- };
3618
- }),
3619
- });
3620
- }
3621
- catch {
3622
- return UNREADABLE_VM_RECONCILE_PEER_TOPOLOGY;
4253
+ vmReconcileObservedCandidatePeerIds(localCgId) {
4254
+ const curatorOrder = this.vmReconcileCuratorPeersByCg.get(localCgId) ?? [];
4255
+ const libp2p = this.node?.libp2p;
4256
+ const getConnections = libp2p?.getConnections;
4257
+ if (typeof getConnections !== 'function') {
4258
+ return curatorOrder.slice(0, DKGAgentBase.VM_RECONCILE_EXACT_ROSTER_MAX);
3623
4259
  }
3624
- }
3625
- async readVmReconcileSwmGen(candidateNamespaces) {
3626
- if (candidateNamespaces.length === 0)
3627
- return 'empty:0';
3628
- try {
3629
- // The changelog cursor is a durable write generation: it survives daemon
3630
- // restart, advances at the store mutation choke point, and is O(1) after
3631
- // its one-time seed. It lets large (> fingerprint cap) stores reuse a
3632
- // proven negative without reconstructing or sampling their SWM content.
3633
- const changelog = asChangelogReader(this.store);
3634
- if (changelog) {
3635
- const head = await changelog.changelogHead({
3636
- priority: 'background',
3637
- source: 'agent.vmReconcile.negativeGeneration',
3638
- });
3639
- return `changelog:${head.era}:${head.seq}`;
3640
- }
3641
- const parts = [];
3642
- const digestRows = (rows) => createHash('sha256').update(rows.join('\n'), 'utf8').digest('hex');
3643
- const maxRows = DKGAgentBase.VM_RECONCILE_SWM_GEN_FINGERPRINT_MAX_ROWS;
3644
- const isTooLarge = (rows) => rows.length > maxRows;
3645
- for (const namespace of candidateNamespaces) {
3646
- const metaGraph = assertSafeIri(namespace.metaGraph);
3647
- const dataGraph = assertSafeIri(namespace.dataGraph);
3648
- const operationRows = await this.store.query(`SELECT ?op ?root ?ts WHERE {
3649
- GRAPH <${metaGraph}> {
3650
- ?op <http://dkg.io/ontology/rootEntity> ?root .
3651
- OPTIONAL { ?op <http://dkg.io/ontology/publishedAt> ?ts . }
3652
- }
3653
- } ORDER BY ?op ?root ?ts LIMIT ${maxRows + 1}`, { source: 'agent.vmReconcile.swmFingerprint.operations' });
3654
- if (operationRows.type !== 'bindings')
3655
- return null;
3656
- if (isTooLarge(operationRows.bindings))
3657
- return null;
3658
- const operations = operationRows.bindings
3659
- .map((row) => [
3660
- String(row['op'] ?? ''),
3661
- String(row['root'] ?? ''),
3662
- String(row['ts'] ?? ''),
3663
- ].join('\0'))
3664
- .sort();
3665
- const dataRows = await this.store.query(`SELECT ?s ?p ?o WHERE {
3666
- GRAPH <${dataGraph}> { ?s ?p ?o . }
3667
- } ORDER BY ?s ?p ?o LIMIT ${maxRows + 1}`, { source: 'agent.vmReconcile.swmFingerprint.data' });
3668
- if (dataRows.type !== 'bindings')
3669
- return null;
3670
- if (isTooLarge(dataRows.bindings))
3671
- return null;
3672
- const dataTriples = dataRows.bindings
3673
- .map((row) => [
3674
- String(row['s'] ?? ''),
3675
- String(row['p'] ?? ''),
3676
- String(row['o'] ?? ''),
3677
- ].join('\0'))
3678
- .sort();
3679
- const privateRootRows = await this.store.query(`SELECT ?privateEntity ?privateRoot WHERE {
3680
- GRAPH <${metaGraph}> {
3681
- ?privateEntity <http://dkg.io/ontology/privateMerkleRoot> ?privateRoot .
3682
- }
3683
- } ORDER BY ?privateEntity ?privateRoot LIMIT ${maxRows + 1}`, { source: 'agent.vmReconcile.swmFingerprint.privateRoots' });
3684
- if (privateRootRows.type !== 'bindings')
3685
- return null;
3686
- if (isTooLarge(privateRootRows.bindings))
3687
- return null;
3688
- const privateRoots = privateRootRows.bindings
3689
- .map((row) => [
3690
- String(row['privateEntity'] ?? ''),
3691
- String(row['privateRoot'] ?? ''),
3692
- ].join('\0'))
3693
- .sort();
3694
- parts.push([
3695
- `meta:${namespace.metaGraph}`,
3696
- `data:${namespace.dataGraph}`,
3697
- `ops:${operations.length}`,
3698
- `opHash:${digestRows(operations)}`,
3699
- `dataTriples:${dataTriples.length}`,
3700
- `dataHash:${digestRows(dataTriples)}`,
3701
- `privateRoots:${privateRoots.length}`,
3702
- `privateRootHash:${digestRows(privateRoots)}`,
3703
- ].join(';'));
3704
- }
3705
- // Catch writes into a newly-created/unregistered namespace that the
3706
- // current namespace enumeration cannot yet name. This process-local term
3707
- // complements (rather than replaces) the content fingerprint: after a
3708
- // restart the fingerprint remains the correctness gate while the counter
3709
- // restarts harmlessly.
3710
- const rootDataGraph = candidateNamespaces[0]?.dataGraph ?? '';
3711
- const swmSuffix = rootDataGraph.indexOf('/_shared_memory');
3712
- const graphPrefix = swmSuffix >= 0 ? `${rootDataGraph.slice(0, swmSuffix)}/` : rootDataGraph;
3713
- const writeRevision = asGraphWriteRevisionSource(this.store)?.getWriteRevision(graphPrefix);
3714
- // A remote mutation can still commit after this fingerprint's reads.
3715
- // Never turn such an in-flight/indeterminate observation into a stable
3716
- // negative-cache key.
3717
- if (writeRevision && !writeRevision.stable)
3718
- return null;
3719
- if (writeRevision)
3720
- parts.push(`writeGen:${writeRevision.generation}`);
3721
- return parts.join('|');
3722
- }
3723
- catch {
3724
- // Probe failures are not a stable SWM generation. Callers must not cache
3725
- // or preserve a negative-cache gate from this result.
3726
- }
3727
- return null;
3728
- }
3729
- vmReconcileSwmGenHasOperations(swmGen) {
3730
- return swmGen.split('|').some((part) => {
3731
- const match = /(?:^|;)ops:(\d+)(?:;|$)/.exec(part);
3732
- return match ? Number(match[1]) > 0 : false;
3733
- });
3734
- }
3735
- vmReconcileSwmGenSupportsDurableNegative(swmGen) {
3736
- // A changelog cursor covers every descendant graph mutation. The fallback
3737
- // fingerprint covers only the bare SWM bucket, so an operation whose data
3738
- // later lands in a per-KA child graph must fail open after restart.
3739
- return swmGen.startsWith('changelog:') || !this.vmReconcileSwmGenHasOperations(swmGen);
3740
- }
3741
- vmReconcileSwmGenContainsSnapshot(cachedSwmGen, currentSwmGen) {
3742
- return cachedSwmGen === currentSwmGen || cachedSwmGen.split('|').includes(currentSwmGen);
3743
- }
3744
- vmReconcileWorkspaceOperationPattern(candidateMetaGraphs) {
3745
- const branches = [];
3746
- for (const graph of candidateMetaGraphs) {
3747
- try {
3748
- branches.push(`{ GRAPH <${assertSafeIri(graph)}> {
3749
- ?op <http://dkg.io/ontology/rootEntity> ?root .
3750
- OPTIONAL { ?op <http://dkg.io/ontology/publishedAt> ?ts . }
3751
- } }`);
3752
- }
3753
- catch {
3754
- // Skip unsafe graph names instead of building a malformed query.
3755
- }
3756
- }
3757
- return branches.join(' UNION ');
3758
- }
3759
- deleteVmReconcileNegativeCacheEntry(cacheKey) {
3760
- const existing = this.vmReconcileNegativeCache.get(cacheKey);
3761
- this.markVmReconcileNegativeCacheHydrated(cacheKey, existing?.localCgId ?? cacheKey.slice(0, Math.max(0, cacheKey.indexOf('\0'))));
3762
- if (existing) {
3763
- this.vmReconcileNegativeCache.delete(cacheKey);
3764
- const keys = this.vmReconcileNegativeCacheKeysByCg.get(existing.localCgId);
3765
- if (keys) {
3766
- keys.delete(cacheKey);
3767
- if (keys.size === 0)
3768
- this.vmReconcileNegativeCacheKeysByCg.delete(existing.localCgId);
3769
- }
3770
- }
3771
- void this.config.contextGraphSubscriptionStore?.deleteVmReconcileNegative?.(cacheKey).catch(() => {
3772
- // The in-memory invalidation remains authoritative for this process.
3773
- });
3774
- }
3775
- indexVmReconcileNegativeCacheEntry(localCgId, cacheKey) {
3776
- let keys = this.vmReconcileNegativeCacheKeysByCg.get(localCgId);
3777
- if (!keys) {
3778
- keys = new Set();
3779
- this.vmReconcileNegativeCacheKeysByCg.set(localCgId, keys);
3780
- }
3781
- keys.add(cacheKey);
3782
- }
3783
- markVmReconcileNegativeCacheHydrated(cacheKey, localCgId) {
3784
- // Access order keeps actively reused keys resident while old one-shot
3785
- // misses fall out. Eviction is fail-open: it only permits another durable
3786
- // lookup if the same key is encountered later.
3787
- this.vmReconcileNegativeCacheHydrated.delete(cacheKey);
3788
- this.vmReconcileNegativeCacheHydrated.set(cacheKey, localCgId);
3789
- while (this.vmReconcileNegativeCacheHydrated.size
3790
- > DKGAgentBase.VM_RECONCILE_CACHE_MAX_ENTRIES) {
3791
- const oldestKey = this.vmReconcileNegativeCacheHydrated.keys().next().value;
3792
- if (oldestKey === undefined)
3793
- break;
3794
- this.vmReconcileNegativeCacheHydrated.delete(oldestKey);
3795
- }
3796
- }
3797
- async shouldDeferVmReconcileByNegativeCache(cacheKey, localCgId) {
3798
- let cached = this.vmReconcileNegativeCache.get(cacheKey);
3799
- const durableStateAlreadyConsulted = this.vmReconcileNegativeCacheHydrated.has(cacheKey);
3800
- this.markVmReconcileNegativeCacheHydrated(cacheKey, localCgId);
3801
- if (!cached && !durableStateAlreadyConsulted) {
3802
- try {
3803
- const durable = await this.config.contextGraphSubscriptionStore
3804
- ?.loadVmReconcileNegative?.(cacheKey);
3805
- const durablePeerTopology = durable
3806
- ? isVmReconcilePeerTopology(durable.peerTopology)
3807
- ? durable.peerTopology
3808
- : parseLegacyVmReconcilePeerTopologyKey(durable.peerTopologyKey)
3809
- : null;
3810
- const durableCleanMissPeerIds = durablePeerTopology
3811
- ? parseVmReconcileCleanMissPeerIds(durable?.cleanMissPeerIds ?? [], durablePeerTopology)
3812
- : null;
3813
- if (durable &&
3814
- durable.cacheKey === cacheKey &&
3815
- durable.localCgId === localCgId &&
3816
- Number.isInteger(durable.failures) && durable.failures > 0 &&
3817
- Number.isFinite(durable.nextRetryAt) &&
3818
- typeof durable.swmGen === 'string' &&
3819
- Array.isArray(durable.candidateNamespaces) &&
3820
- durable.candidateNamespaces.every((item) => typeof item?.metaGraph === 'string' && typeof item?.dataGraph === 'string') &&
3821
- durablePeerTopology !== null &&
3822
- durableCleanMissPeerIds !== null) {
3823
- if (!this.vmReconcileSwmGenSupportsDurableNegative(durable.swmGen)) {
3824
- await this.config.contextGraphSubscriptionStore
3825
- ?.deleteVmReconcileNegative?.(cacheKey);
3826
- }
3827
- else {
3828
- cached = {
3829
- localCgId: durable.localCgId,
3830
- failures: durable.failures,
3831
- nextRetryAt: durable.nextRetryAt,
3832
- swmGen: durable.swmGen,
3833
- candidateNamespaces: durable.candidateNamespaces,
3834
- peerTopology: durablePeerTopology,
3835
- cleanMissPeerIds: durableCleanMissPeerIds,
3836
- };
3837
- this.vmReconcileNegativeCache.set(cacheKey, cached);
3838
- this.indexVmReconcileNegativeCacheEntry(localCgId, cacheKey);
3839
- }
3840
- }
3841
- }
3842
- catch {
3843
- // Persistence is an accelerator only. Fail open to an authoritative scan.
3844
- }
3845
- }
3846
- if (!cached)
3847
- return false;
3848
- if (Date.now() >= cached.nextRetryAt)
3849
- return false;
3850
- try {
3851
- try {
3852
- await this.primeCatchupConnections();
3853
- }
3854
- catch {
3855
- // Best effort only; an unchanged connection view can still honor the
3856
- // cached miss until the backoff expires.
3857
- }
3858
- const currentPeerTopology = await this.vmReconcilePeerTopology(localCgId);
3859
- if (!canReuseVmReconcilePeerTopology({
3860
- topology: cached.peerTopology,
3861
- cleanMissPeerIds: cached.cleanMissPeerIds,
3862
- }, currentPeerTopology)) {
3863
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3864
- this.clearVmReconcileActiveFetchCooldown(localCgId);
3865
- return false;
3866
- }
3867
- const currentNamespaces = await this.collectVmReconcileSwmCandidateNamespacesBestEffort(localCgId);
3868
- const currentNamespaceKey = this.vmReconcileSwmNamespaceKey(currentNamespaces.namespaces);
3869
- const cachedNamespaceKey = this.vmReconcileSwmNamespaceKey(cached.candidateNamespaces);
3870
- if (currentNamespaceKey !== cachedNamespaceKey) {
3871
- if (!currentNamespaces.complete) {
3872
- const currentSwmGen = await this.readVmReconcileSwmGen(currentNamespaces.namespaces);
3873
- if (currentSwmGen === null) {
3874
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3875
- return false;
3876
- }
3877
- if (!this.vmReconcileSwmGenContainsSnapshot(cached.swmGen, currentSwmGen)) {
3878
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3879
- return false;
3880
- }
3881
- return true;
3882
- }
3883
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3884
- return false;
3885
- }
3886
- const pattern = this.vmReconcileWorkspaceOperationPattern(currentNamespaces.namespaces.map((namespace) => namespace.metaGraph));
3887
- if (!pattern)
3888
- return true;
3889
- const currentSwmGen = await this.readVmReconcileSwmGen(currentNamespaces.namespaces);
3890
- if (currentSwmGen === null) {
3891
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3892
- return false;
3893
- }
3894
- if (currentSwmGen !== cached.swmGen) {
3895
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3896
- return false;
3897
- }
3898
- }
3899
- catch {
3900
- // Unexpected validation failures leave the existing backoff in place;
3901
- // expected generation probe failures return null and clear the gate above.
3902
- }
3903
- return true;
3904
- }
3905
- recordVmReconcileNegativeCache(cacheKey, localCgId, state) {
3906
- if (state.swmGen === null) {
3907
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
3908
- return;
3909
- }
3910
- this.pruneVmReconcileState();
3911
- const previous = this.vmReconcileNegativeCache.get(cacheKey);
3912
- const failures = (previous?.failures ?? 0) + 1;
3913
- const exponentialBackoff = Math.min(DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_MAX_MS, DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_BASE_MS * 2 ** Math.max(0, failures - 1));
3914
- const jitterSample = createHash('sha256')
3915
- .update(`${this.node.peerId.toString()}\0${cacheKey}\0${failures}`)
3916
- .digest()
3917
- .readUInt32BE(0) / 0x1_0000_0000;
3918
- const backoff = Math.min(DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_MAX_MS, Math.max(1, Math.round(exponentialBackoff * (0.8 + jitterSample * 0.4))));
3919
- getMetrics().storeRetryAttemptsTotal.add(1, {
3920
- scope: 'vm_reconcile',
3921
- reason: 'no_swm',
3922
- attempt: Math.min(failures, 16),
3923
- });
3924
- if (previous) {
3925
- // Replace in place without racing an asynchronous durable DELETE against
3926
- // the SAVE below. The record key/local CG are unchanged here.
3927
- this.vmReconcileNegativeCache.delete(cacheKey);
3928
- const keys = this.vmReconcileNegativeCacheKeysByCg.get(previous.localCgId);
3929
- keys?.delete(cacheKey);
3930
- if (keys?.size === 0)
3931
- this.vmReconcileNegativeCacheKeysByCg.delete(previous.localCgId);
3932
- }
3933
- const record = {
3934
- localCgId,
3935
- failures,
3936
- nextRetryAt: Date.now() + backoff,
3937
- swmGen: state.swmGen,
3938
- candidateNamespaces: state.candidateNamespaces,
3939
- peerTopology: state.peerTopology,
3940
- cleanMissPeerIds: state.cleanMissPeerIds,
3941
- };
3942
- this.vmReconcileNegativeCache.set(cacheKey, record);
3943
- this.markVmReconcileNegativeCacheHydrated(cacheKey, localCgId);
3944
- this.indexVmReconcileNegativeCacheEntry(localCgId, cacheKey);
3945
- const durableStore = this.config.contextGraphSubscriptionStore;
3946
- if (this.vmReconcileSwmGenSupportsDurableNegative(record.swmGen)) {
3947
- void durableStore?.saveVmReconcileNegative?.({
3948
- cacheKey,
3949
- localCgId: record.localCgId,
3950
- failures: record.failures,
3951
- nextRetryAt: record.nextRetryAt,
3952
- swmGen: record.swmGen,
3953
- candidateNamespaces: record.candidateNamespaces,
3954
- peerTopologyKey: encodeLegacyVmReconcilePeerTopologyKey(record.peerTopology),
3955
- peerTopology: record.peerTopology,
3956
- cleanMissPeerIds: record.cleanMissPeerIds,
3957
- }).catch(() => {
3958
- // Persistence is an accelerator only; the process-local gate still works.
3959
- });
3960
- }
3961
- else {
3962
- void durableStore?.deleteVmReconcileNegative?.(cacheKey).catch(() => {
3963
- // Fail open after restart even if best-effort cleanup cannot complete.
3964
- });
3965
- }
3966
- this.pruneVmReconcileState();
3967
- }
3968
- vmReconcileRotationNow() {
3969
- return performance.now();
3970
- }
3971
- vmReconcileRotationSlotKey(target) {
3972
- return `${target.localCgId}\0${target.onChainCgId}\0${target.ordinal}`;
3973
- }
3974
- clearVmReconcileRotationStateForSlot(localCgId, onChainCgId, ordinal) {
3975
- this.vmReconcileRotationState.delete(`${localCgId}\0${onChainCgId.toString()}\0${ordinal}`);
3976
- }
3977
- vmReconcileRotationFingerprint(target) {
3978
- return `${target.ual}\0${target.merkleRoot.toLowerCase()}`;
3979
- }
3980
- vmReconcileObservedCandidatePeerIds(localCgId) {
3981
- const curatorOrder = this.vmReconcileCuratorPeersByCg.get(localCgId) ?? [];
3982
- const libp2p = this.node?.libp2p;
3983
- const getConnections = libp2p?.getConnections;
3984
- if (typeof getConnections !== 'function') {
3985
- return curatorOrder.slice(0, DKGAgentBase.VM_RECONCILE_EXACT_ROSTER_MAX);
3986
- }
3987
- const peersById = new Map();
3988
- for (const connection of getConnections.call(libp2p)) {
3989
- const peer = connection.remotePeer;
3990
- const peerId = peer?.toString();
3991
- if (!peer || !peerId || peerId === this.peerId || peersById.has(peerId))
3992
- continue;
3993
- peersById.set(peerId, peer);
4260
+ const peersById = new Map();
4261
+ for (const connection of getConnections.call(libp2p)) {
4262
+ const peer = connection.remotePeer;
4263
+ const peerId = peer?.toString();
4264
+ if (!peer || !peerId || peerId === this.peerId || peersById.has(peerId))
4265
+ continue;
4266
+ peersById.set(peerId, peer);
3994
4267
  }
3995
4268
  // `getConnections()` iteration order is transport state, not candidate
3996
4269
  // identity. Sort before tiering/capping so harmless connection reorder can
@@ -4244,11 +4517,26 @@ export class SwmHostModeMethods extends DKGAgentBase {
4244
4517
  * mutable transition so partial responses revoke affinity consistently.
4245
4518
  * Unavailable peers and the global considered-peer cap remain authoritative.
4246
4519
  */
4247
- selectVmReconcileExactCandidate(record, fallbackCandidatePeerIds, policy) {
4520
+ selectVmReconcileExactCandidate(record, fallbackCandidatePeerIds, policy, binding) {
4248
4521
  const uncreditedCandidateOrder = record
4249
4522
  ? this.vmReconcileUncreditedCandidateOrder(record)
4250
4523
  : fallbackCandidatePeerIds;
4251
- return policy.selectNextCandidate(uncreditedCandidateOrder, DKGAgentBase.VM_RECONCILE_EXACT_PEER_MAX);
4524
+ const preferredPeerId = binding
4525
+ ? this.vmReconcilePublicCoreTransportPreferencePolicy.preferredPeer(binding.localCgId, binding.onChainCgId, uncreditedCandidateOrder)
4526
+ : undefined;
4527
+ const now = this.vmReconcileRotationNow();
4528
+ const { order, heldOffPeerIds } = orderVmRecoveryCandidates({
4529
+ candidatePeerIds: uncreditedCandidateOrder,
4530
+ preferredPeerId,
4531
+ streamPeerIds: binding?.experimentalStreamPeerIds,
4532
+ lastAttemptedPeerId: record?.lastAttemptedPeerId,
4533
+ hasSetback: (peerId) => binding !== undefined
4534
+ && record?.streamSetbackPeerIds?.has(peerId) === true
4535
+ && this.vmReconcileStreamSetbackPolicy.inSetbackStreak(binding.localCgId, peerId, now),
4536
+ isHeldOff: (peerId) => binding !== undefined
4537
+ && this.vmReconcileStreamSetbackPolicy.heldOff(binding.localCgId, peerId, now),
4538
+ });
4539
+ return policy.selectNextCandidate(order, DKGAgentBase.VM_RECONCILE_EXACT_PEER_MAX, heldOffPeerIds);
4252
4540
  }
4253
4541
  findVmReconcileRotationReplacement(requestingCgId) {
4254
4542
  if (this.vmReconcileRotationState.size < DKGAgentBase.VM_RECONCILE_CACHE_MAX_ENTRIES) {
@@ -4319,7 +4607,11 @@ export class SwmHostModeMethods extends DKGAgentBase {
4319
4607
  if (key.startsWith(prefix))
4320
4608
  this.vmReconcileRotationState.delete(key);
4321
4609
  }
4610
+ this.vmReconcileTransportBudgetPolicy.forgetContextGraph(localCgId);
4611
+ this.vmReconcileStreamSetbackPolicy.forgetContextGraph(localCgId);
4322
4612
  this.vmReconcileRotationAdmissionCursorByCg.delete(localCgId);
4613
+ this.vmReconcilePublicCoreTransportPreferencePolicy?.forgetContextGraph(localCgId);
4614
+ existingVmRecoveryPreparation(this)?.discard(localCgId);
4323
4615
  }
4324
4616
  closeVmReconcileRotationState() {
4325
4617
  this.vmReconcileLifecycleController?.abort();
@@ -4330,10 +4622,22 @@ export class SwmHostModeMethods extends DKGAgentBase {
4330
4622
  // without running the base constructor. Shutdown must remain best-effort
4331
4623
  // for that supported test seam and never mask later teardown failures.
4332
4624
  this.vmReconcileRotationState?.clear();
4625
+ this.vmReconcileTransportBudgetPolicy?.clear();
4626
+ this.vmReconcileStreamSetbackPolicy?.clear();
4333
4627
  this.vmReconcileRotationAdmissionCursorByCg?.clear();
4334
4628
  this.vmReconcileCuratorPeersByCg?.clear();
4335
4629
  this.vmReconcileCuratorPageCursorByCg?.clear();
4336
4630
  this.vmReconcileExactPeerCapabilities?.clear();
4631
+ this.vmReconcilePublicCoreTransportPreferencePolicy?.clear();
4632
+ this.vmRefreshQueue?.clear();
4633
+ // Cancel speculative sizing reads and let shutdown wait for them to settle.
4634
+ const preparation = existingVmRecoveryPreparation(this);
4635
+ if (preparation && this.vmReconcilePhysicalRuns) {
4636
+ trackVmReconcilePhysicalRun(this.vmReconcilePhysicalRuns, preparation.close());
4637
+ }
4638
+ else {
4639
+ void preparation?.close();
4640
+ }
4337
4641
  }
4338
4642
  openVmReconcileRotationState() {
4339
4643
  if (!this.vmReconcileLifecycleController || this.vmReconcileLifecycleController.signal.aborted) {
@@ -4360,6 +4664,8 @@ export class SwmHostModeMethods extends DKGAgentBase {
4360
4664
  return;
4361
4665
  if (peerId !== undefined) {
4362
4666
  capturedRecord.attemptedPeerIds.add(peerId);
4667
+ // This attempt gave a verdict: an earlier setback with the peer is history.
4668
+ capturedRecord.streamSetbackPeerIds?.delete(peerId);
4363
4669
  if (disposition === 'clean-absent')
4364
4670
  capturedRecord.cleanAbsentPeerIds.add(peerId);
4365
4671
  // Preserve fairly accumulated proof progress while other targets share
@@ -4384,6 +4690,23 @@ export class SwmHostModeMethods extends DKGAgentBase {
4384
4690
  }
4385
4691
  this.touchVmReconcileRotationRecord(slotKey, capturedRecord);
4386
4692
  }
4693
+ /**
4694
+ * Hand a target its turn at `peerId` back after an attempt that ended
4695
+ * without a verdict on the peer's data. The peer is uncredited again, so the
4696
+ * cycle cannot complete, and so cannot back off, without asking it. Removing
4697
+ * a mark is safe against any roster, which is why membership is not checked.
4698
+ * The setback is noted on the target, for candidate selection to order by.
4699
+ */
4700
+ releaseVmReconcileRotationAttempt(target, peerId, capturedRecord) {
4701
+ if (this.vmReconcileRotationClosed)
4702
+ return;
4703
+ const slotKey = this.vmReconcileRotationSlotKey(target);
4704
+ if (this.vmReconcileRotationState.get(slotKey) !== capturedRecord)
4705
+ return;
4706
+ capturedRecord.attemptedPeerIds.delete(peerId);
4707
+ (capturedRecord.streamSetbackPeerIds ??= new Set()).add(peerId);
4708
+ this.touchVmReconcileRotationRecord(slotKey, capturedRecord);
4709
+ }
4387
4710
  creditVmReconcileCleanAbsence(target, peerId, expectedCandidatePeerIds, capturedRecord) {
4388
4711
  this.settleVmReconcileRotationAttempt(target, peerId, 'clean-absent', expectedCandidatePeerIds, capturedRecord);
4389
4712
  }
@@ -4414,26 +4737,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
4414
4737
  this.installVmReconcileActiveFetchCooldown(localCgId, now);
4415
4738
  return true;
4416
4739
  }
4417
- vmReconcileActiveFetchHadUsableResponse(result) {
4418
- if ((result.peersSucceeded ?? 0) > 0)
4419
- return true;
4420
- if ((result.sharedMemorySynced ?? 0) > 0)
4421
- return true;
4422
- const shared = result.diagnostics?.sharedMemory;
4423
- if (!shared)
4424
- return false;
4425
- return (shared.insertedDataTriples ?? 0) > 0
4426
- || (shared.insertedMetaTriples ?? 0) > 0
4427
- || (shared.checkpointAdvances ?? 0) > 0
4428
- || ((shared.completedPhases ?? 0) > 0 && (shared.resumedPhases ?? 0) > 0);
4429
- }
4430
4740
  pruneVmReconcileState(now = Date.now()) {
4431
- while (this.vmReconcileNegativeCache.size > DKGAgentBase.VM_RECONCILE_CACHE_MAX_ENTRIES) {
4432
- const oldestKey = this.vmReconcileNegativeCache.keys().next().value;
4433
- if (oldestKey === undefined)
4434
- break;
4435
- this.deleteVmReconcileNegativeCacheEntry(oldestKey);
4436
- }
4437
4741
  for (const [localCgId, cooldown] of this.vmReconcileFetchCooldowns) {
4438
4742
  if (now - cooldown.startedAt >= DKGAgentBase.VM_RECONCILE_SWEEP_INTERVAL_MS) {
4439
4743
  this.clearVmReconcileActiveFetchCooldown(localCgId);
@@ -4492,22 +4796,6 @@ export class SwmHostModeMethods extends DKGAgentBase {
4492
4796
  });
4493
4797
  }
4494
4798
  forceClearVmReconcileStateForContextGraph(localCgId, options = {}) {
4495
- const negativeCacheKeys = this.vmReconcileNegativeCacheKeysByCg.get(localCgId);
4496
- if (negativeCacheKeys) {
4497
- for (const cacheKey of Array.from(negativeCacheKeys)) {
4498
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
4499
- }
4500
- }
4501
- for (const [cacheKey, hydratedLocalCgId] of this.vmReconcileNegativeCacheHydrated) {
4502
- if (hydratedLocalCgId === localCgId) {
4503
- this.vmReconcileNegativeCacheHydrated.delete(cacheKey);
4504
- }
4505
- }
4506
- void this.config.contextGraphSubscriptionStore
4507
- ?.deleteVmReconcileNegativesForContextGraph?.(localCgId)
4508
- .catch(() => {
4509
- // Best-effort durable cleanup; generation checks still reject stale rows.
4510
- });
4511
4799
  this.reconcileCursors.delete(localCgId);
4512
4800
  if (options.includeSelectedCursor !== false
4513
4801
  && this.selectedVmReconcileCursors.delete(localCgId)) {
@@ -4520,6 +4808,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
4520
4808
  this.vmReconcileCatchupPeerCursor.delete(localCgId);
4521
4809
  this.vmReconcileCatchupPeerOrder.delete(localCgId);
4522
4810
  this.clearRecentVmReconcileStateForContextGraph(localCgId);
4811
+ this.vmRefreshQueue?.clearContextGraph(localCgId);
4523
4812
  }
4524
4813
  clearRecentVmReconcileStateForContextGraph(localCgId) {
4525
4814
  this.recentReconciledUals.deleteByPrefix(`${localCgId}\0`);
@@ -4595,97 +4884,120 @@ export class SwmHostModeMethods extends DKGAgentBase {
4595
4884
  }
4596
4885
  pruneVmReconcileCacheKeySiblings(cacheKey) {
4597
4886
  const prefix = this.vmReconcileCacheKeyPrefix(cacheKey);
4598
- for (const key of this.vmReconcileNegativeCache.keys()) {
4599
- if (key !== cacheKey && key.startsWith(prefix)) {
4600
- this.deleteVmReconcileNegativeCacheEntry(key);
4601
- }
4602
- }
4603
4887
  this.recentReconciledUals.deleteByPrefix(prefix, cacheKey);
4604
4888
  }
4605
- vmReconcileConnectedPeerCount() {
4606
- try {
4607
- const libp2p = this.node?.libp2p;
4608
- const getConnections = libp2p?.getConnections;
4609
- if (typeof getConnections !== 'function')
4610
- return 0;
4611
- const uniquePeers = new Set(getConnections.call(libp2p)
4612
- .map((connection) => connection.remotePeer?.toString())
4613
- .filter((peerId) => typeof peerId === 'string' && peerId.length > 0));
4614
- return uniquePeers.size;
4615
- }
4616
- catch {
4617
- return 0;
4618
- }
4619
- }
4620
4889
  /**
4621
- * Execute one already-admitted provider attempt. The caller owns
4622
- * roster/admission, packing, and provider affinity; this executor owns the
4623
- * exact request or cached legacy fallback, per-UAL chain revalidation, and
4624
- * rotation settlement.
4890
+ * Execute one provider after connection/network readiness. The caller owns
4891
+ * roster, packing, and provider affinity; local sync admission starts the
4892
+ * physical attempt before rotation marks are installed. This executor owns
4893
+ * exact or cached legacy work, per-UAL chain revalidation, and settlement.
4625
4894
  * Returns immutable evidence for the caller to merge. A stale lifecycle is
4626
4895
  * discriminated by whether the physical attempt had already been admitted;
4627
4896
  * only the pre-admission variant guarantees zero attempt side effects.
4628
4897
  */
4629
4898
  async executeVmRecoveryBatch(input) {
4630
- const { localCgId, onChainCgId, peerId, attempts, unavailablePeerIds, headBlock, signal, isRecoveryCurrent, revalidateTarget, ctx, } = input;
4899
+ const { localCgId, onChainCgId, peerId, attempts, unavailablePeerIds, headBlock, signal, isRecoveryCurrent, revalidateTarget, ctx, exactRecoveryTransportMode = 'stream-preferred', legacyAttemptTimeoutMs, registeredPublicEvidence, phases, } = input;
4631
4900
  const unavailablePeerIdSet = new Set(unavailablePeerIds);
4632
4901
  // The caller's last guard can race an unsubscribe/rebind before this
4633
4902
  // executor begins. Do not retain attempt evidence or emit a fetch event
4634
4903
  // until this exact lifecycle has been re-proved at the ownership boundary.
4635
4904
  if (!isRecoveryCurrent())
4636
4905
  return { kind: 'not-started-stale' };
4637
- const useCachedLegacyFallback = this.vmReconcileExactFilterUnsupported(peerId);
4906
+ const useCachedLegacyFallback = exactRecoveryTransportMode !== 'stream-required'
4907
+ && this.vmReconcileExactFilterUnsupported(peerId);
4638
4908
  const handledOrdinals = [];
4639
4909
  const attemptedOrdinals = [];
4640
4910
  const outcomes = [];
4641
- for (const attempt of attempts) {
4642
- const batchTarget = attempt.entry.target;
4643
- handledOrdinals.push(batchTarget.ordinal);
4644
- attemptedOrdinals.push(batchTarget.ordinal);
4645
- if (attempt.installedRecord) {
4646
- attempt.installedRecord.lastAttemptedPeerId = peerId;
4647
- attempt.installedRecord.attemptedPeerIds.add(peerId);
4648
- attempt.installedRecord.collectionDeadlineAt = this.vmReconcileRotationNow()
4649
- + DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_MAX_MS;
4650
- this.touchVmReconcileRotationRecord(this.vmReconcileRotationSlotKey(batchTarget), attempt.installedRecord);
4911
+ let workStarted = false;
4912
+ const onWorkStarted = () => {
4913
+ if (workStarted || !isRecoveryCurrent())
4914
+ return;
4915
+ workStarted = true;
4916
+ for (const attempt of attempts) {
4917
+ const batchTarget = attempt.entry.target;
4918
+ this.vmReconcileTransportBudgetPolicy.recordAdmitted(batchTarget, peerId);
4919
+ handledOrdinals.push(batchTarget.ordinal);
4920
+ attemptedOrdinals.push(batchTarget.ordinal);
4921
+ const record = attempt.installedRecord;
4922
+ const slotKey = this.vmReconcileRotationSlotKey(batchTarget);
4923
+ if (record && this.vmReconcileRotationState.get(slotKey) === record
4924
+ && record.fingerprint === this.vmReconcileRotationFingerprint(batchTarget)) {
4925
+ record.lastAttemptedPeerId = peerId;
4926
+ record.attemptedPeerIds.add(peerId);
4927
+ record.collectionDeadlineAt = this.vmReconcileRotationNow()
4928
+ + DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_MAX_MS;
4929
+ this.touchVmReconcileRotationRecord(slotKey, record);
4930
+ }
4931
+ this.emitReplication({
4932
+ contextGraphId: localCgId,
4933
+ onChainCgId: onChainCgId.toString(),
4934
+ action: 'fetch',
4935
+ ordinal: batchTarget.ordinal,
4936
+ kaId: batchTarget.kaId,
4937
+ ual: batchTarget.ual,
4938
+ detail: useCachedLegacyFallback
4939
+ ? 'legacy-sync'
4940
+ : attempts.length > 1 ? 'exact-asset-batch' : 'exact-asset',
4941
+ });
4651
4942
  }
4652
- this.emitReplication({
4653
- contextGraphId: localCgId,
4654
- onChainCgId: onChainCgId.toString(),
4655
- action: 'fetch',
4656
- ordinal: batchTarget.ordinal,
4657
- kaId: batchTarget.kaId,
4658
- ual: batchTarget.ual,
4659
- detail: useCachedLegacyFallback
4660
- ? 'legacy-sync'
4661
- : attempts.length > 1 ? 'exact-asset-batch' : 'exact-asset',
4662
- });
4663
- }
4943
+ };
4664
4944
  let disposition = 'incomplete';
4945
+ let streamOutcome;
4946
+ let localAdmissionDeferred = false;
4947
+ const legacyAttemptStartedAt = Date.now();
4948
+ const remainingLegacyAttemptMs = () => legacyAttemptTimeoutMs === undefined
4949
+ ? undefined
4950
+ : Math.max(0, legacyAttemptTimeoutMs - (Date.now() - legacyAttemptStartedAt));
4665
4951
  const runLegacyFallback = async () => {
4952
+ const remainingMs = remainingLegacyAttemptMs();
4953
+ // Both the exact-filter probe and a full-scan fallback belong to one
4954
+ // provider turn. Do not grant a fresh long deadline after the probe has
4955
+ // already spent the bounded legacy window.
4956
+ if (remainingMs !== undefined && remainingMs < SYNC_MIN_GRAPH_BUDGET_MS) {
4957
+ this.log.info(ctx, `VM legacy fallback for "${localCgId}" from ${peerId.slice(-8)} skipped: attempt budget exhausted`);
4958
+ return;
4959
+ }
4666
4960
  try {
4667
4961
  const fallback = await this.runLegacyDurableSyncDetailed(ctx, peerId, [localCgId], undefined, undefined, undefined, {
4668
4962
  stopOnBackoffWorthyFailure: true,
4669
- priority: 1_000,
4963
+ priority: VM_RECOVERY_SYNC_PRIORITY,
4670
4964
  source: 'vm-recovery',
4965
+ onWorkStarted,
4671
4966
  signal,
4672
4967
  isCurrent: isRecoveryCurrent,
4968
+ ...(remainingMs === undefined ? {} : { totalTimeoutMs: remainingMs }),
4673
4969
  });
4970
+ if (fallback.admission === 'work-started')
4971
+ onWorkStarted();
4972
+ // An admitted exact request retains its attempt through a refused
4973
+ // fallback. Only the original never-started operation can defer.
4974
+ localAdmissionDeferred = !workStarted && fallback.admission === 'local-admission-deferred';
4674
4975
  this.log.info(ctx, `VM legacy fallback for "${localCgId}" from ${peerId.slice(-8)}: fetched=${fallback.result.fetchedDataTriples + fallback.result.fetchedMetaTriples} inserted=${fallback.result.insertedTriples} failed=${fallback.result.failedPeers + fallback.result.failedPhases}`);
4675
4976
  }
4676
4977
  catch (fallbackError) {
4677
4978
  this.log.info(ctx, `VM legacy fallback for "${localCgId}" from ${peerId.slice(-8)} failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
4678
4979
  }
4679
4980
  };
4981
+ const exchangeInterval = phases?.begin();
4680
4982
  try {
4681
4983
  if (useCachedLegacyFallback) {
4682
4984
  await runLegacyFallback();
4683
4985
  }
4684
4986
  else {
4685
- const detailed = await this.syncExactKnowledgeAssetsFromPeerDetailed(peerId, localCgId, attempts.map(({ entry }) => entry.target.ual), { signal, isCurrent: isRecoveryCurrent });
4987
+ const detailed = await this.syncExactKnowledgeAssetsFromPeerDetailed(peerId, localCgId, attempts.map(({ entry }) => entry.target.ual), {
4988
+ signal, isCurrent: isRecoveryCurrent, onWorkStarted, exactRecoveryTransportMode,
4989
+ ...(legacyAttemptTimeoutMs === undefined ? {} : { totalTimeoutMs: legacyAttemptTimeoutMs }),
4990
+ ...(registeredPublicEvidence ? { registeredPublicEvidence } : {}),
4991
+ });
4686
4992
  const { result } = detailed;
4687
4993
  disposition = detailed.disposition;
4688
- if (detailed.responderCapability === 'legacy-filter-unsupported') {
4994
+ streamOutcome = detailed.streamOutcome;
4995
+ // A fresh capability response is physical peer evidence even when
4996
+ // its later ordinary fallback cannot acquire local capacity.
4997
+ if (detailed.admission === 'work-started' || detailed.responderCapability !== undefined)
4998
+ onWorkStarted();
4999
+ localAdmissionDeferred = !workStarted && detailed.admission === 'local-admission-deferred';
5000
+ if (exactRecoveryTransportMode !== 'stream-required' && detailed.responderCapability === 'legacy-filter-unsupported') {
4689
5001
  this.rememberVmReconcileExactFilterUnsupported(peerId);
4690
5002
  if (isRecoveryCurrent() && disposition === 'incomplete') {
4691
5003
  await runLegacyFallback();
@@ -4697,18 +5009,44 @@ export class SwmHostModeMethods extends DKGAgentBase {
4697
5009
  catch (error) {
4698
5010
  this.log.info(ctx, `VM exact fetch for "${localCgId}" from ${peerId.slice(-8)} failed: ${error instanceof Error ? error.message : String(error)}`);
4699
5011
  }
5012
+ if (phases && exchangeInterval)
5013
+ phases.chargeSince('exchange', exchangeInterval.startedAt, exchangeInterval.before);
4700
5014
  if (!isRecoveryCurrent())
4701
- return { kind: 'stale-after-attempt' };
5015
+ return { kind: workStarted ? 'stale-after-attempt' : 'not-started-stale' };
5016
+ if (localAdmissionDeferred)
5017
+ return { kind: 'local-admission-deferred' };
5018
+ // A stream that answered busy or broke says nothing about the peer's data.
5019
+ // The policy decides whether the targets still pending keep their turn at it.
5020
+ let providerTurnKept = false;
5021
+ if (workStarted && streamOutcome !== undefined) {
5022
+ if (streamOutcome === 'complete') {
5023
+ this.vmReconcileStreamSetbackPolicy.recordServed(localCgId, peerId, this.vmReconcileRotationNow());
5024
+ }
5025
+ else {
5026
+ const setback = this.vmReconcileStreamSetbackPolicy.recordSetback(localCgId, peerId, streamOutcome, this.vmReconcileRotationNow());
5027
+ providerTurnKept = setback.keepsTurn;
5028
+ observeVmRecoveryTiming(() => this.log.info(ctx, `VM exact recovery stream setback for "${localCgId}" from ${peerId.slice(-8)}: `
5029
+ + `kind=${streamOutcome} assets=${attempts.length} keepsTurn=${setback.keepsTurn ? 1 : 0} `
5030
+ + `holdOffMs=${setback.holdOffMs}`));
5031
+ }
5032
+ }
4702
5033
  const perUalDispositions = new Map();
4703
- for (const attempt of attempts) {
5034
+ // The batch's targets are re-verified side by side, with the scan's own
5035
+ // bound, so their chain reads can leave in one request. Like the scan, a
5036
+ // target that throws stops the ones not yet started and the batch rejects
5037
+ // only once the ones in flight have finished: nothing here outlives the
5038
+ // pass. They are settled in order below: each step still sees the rotation
5039
+ // state its predecessor left.
5040
+ const revalidateBatch = () => mapWithConcurrencyDrained(attempts, DKGAgentBase.VM_RECONCILE_ORDINAL_CONCURRENCY, (attempt) => this.reconcileChainOrdinal(localCgId, onChainCgId, attempt.entry.target.ordinal, headBlock, {
5041
+ isTargetCurrent: isRecoveryCurrent,
5042
+ revalidateTarget,
5043
+ }));
5044
+ const revalidated = await (phases ? phases.measure('post-fetch', revalidateBatch) : revalidateBatch());
5045
+ if (!isRecoveryCurrent())
5046
+ return { kind: 'stale-after-attempt' };
5047
+ for (const [attemptIndex, attempt] of attempts.entries()) {
4704
5048
  const batchTarget = attempt.entry.target;
4705
- const outcome = await this.reconcileChainOrdinal(localCgId, onChainCgId, batchTarget.ordinal, headBlock, {
4706
- isTargetCurrent: isRecoveryCurrent,
4707
- revalidateTarget,
4708
- deferActiveFetch: true,
4709
- });
4710
- if (!isRecoveryCurrent())
4711
- return { kind: 'stale-after-attempt' };
5049
+ const outcome = revalidated[attemptIndex];
4712
5050
  outcomes.push([batchTarget.ordinal, outcome]);
4713
5051
  const perTargetDisposition = (outcome.status === 'reconciled' || outcome.status === 'already')
4714
5052
  ? 'found'
@@ -4717,6 +5055,8 @@ export class SwmHostModeMethods extends DKGAgentBase {
4717
5055
  : 'incomplete';
4718
5056
  perUalDispositions.set(batchTarget.ual, perTargetDisposition);
4719
5057
  if (outcome.status === 'pending' && outcome.recovery) {
5058
+ if (!workStarted)
5059
+ continue;
4720
5060
  const batchRecord = attempt.installedRecord;
4721
5061
  if (!batchRecord || this.vmReconcileRotationClosed)
4722
5062
  continue;
@@ -4724,10 +5064,14 @@ export class SwmHostModeMethods extends DKGAgentBase {
4724
5064
  !== batchRecord)
4725
5065
  continue;
4726
5066
  const candidateMembershipAfter = this.vmReconcileObservedCandidatePeerIds(localCgId);
5067
+ if (providerTurnKept) {
5068
+ this.releaseVmReconcileRotationAttempt(batchTarget, peerId, batchRecord);
5069
+ }
4727
5070
  if (!this.vmReconcilePeerMembershipMatches(batchRecord.candidatePeerIds, candidateMembershipAfter)) {
4728
5071
  this.prepareVmReconcileRotationTarget(outcome.recovery, candidateMembershipAfter, this.vmReconcileRotationNow());
4729
5072
  }
4730
- else if (this.vmReconcileRecoveryTargetMatches(batchTarget, outcome.recovery)) {
5073
+ else if (!providerTurnKept
5074
+ && this.vmReconcileRecoveryTargetMatches(batchTarget, outcome.recovery)) {
4731
5075
  this.settleVmReconcileRotationAttempt(batchTarget, peerId, perTargetDisposition, attempt.candidatePeerIds, batchRecord, unavailablePeerIdSet);
4732
5076
  }
4733
5077
  }
@@ -4742,6 +5086,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
4742
5086
  attemptedOrdinals,
4743
5087
  providerDisposition: disposition,
4744
5088
  perUalDispositions: [...perUalDispositions],
5089
+ providerTurnKept,
4745
5090
  };
4746
5091
  }
4747
5092
  /**
@@ -4757,6 +5102,26 @@ export class SwmHostModeMethods extends DKGAgentBase {
4757
5102
  && this.vmReconcileLifecycleGeneration === rotationGeneration
4758
5103
  && isTargetCurrent();
4759
5104
  const ctx = createOperationContext('system');
5105
+ // Advisory sizing preparation (opt-in, default off). Created lazily and
5106
+ // owned by this host; every hint is bound to this exact recovery operation.
5107
+ const readUpdateContextForPreparation = this.chain.getKnowledgeAssetUpdateContext;
5108
+ const experimentPolicy = resolveVmRecoveryExperimentPolicy(this.config.vmRecoveryPrefetchEnabled, existingVmRecoveryPreparation(this)?.limits);
5109
+ const preparation = experimentPolicy.prefetchEnabled
5110
+ ? vmRecoveryPreparationFor(this, typeof readUpdateContextForPreparation === 'function'
5111
+ ? { readUpdateContext: (id, readOptions) => readUpdateContextForPreparation.call(this.chain, id, readOptions) }
5112
+ : null)
5113
+ : undefined;
5114
+ const preparationScope = {
5115
+ localCgId,
5116
+ onChainCgId,
5117
+ generation: rotationGeneration,
5118
+ ...(signal ? { signal } : {}),
5119
+ isCurrent: isRecoveryCurrent,
5120
+ };
5121
+ const transportPreparation = preparation
5122
+ ? new VmRecoveryTransportPreparation(preparation, preparationScope) : undefined;
5123
+ const timing = new VmRecoveryTimingObserver(localCgId, (message) => this.log.info(ctx, message));
5124
+ const phases = timing.phases;
4760
5125
  const noRecovery = (continuationOrdinal, cooldownOnly = false) => ({
4761
5126
  outcomes: new Map(),
4762
5127
  attemptedOrdinals: [],
@@ -4889,7 +5254,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
4889
5254
  ...(this.vmReconcileCuratorPeersByCg.get(localCgId) ?? []),
4890
5255
  ];
4891
5256
  const curatorPageCursor = this.vmReconcileCuratorPageCursorByCg.get(localCgId);
4892
- const curatorResolution = await this.resolveCuratorPeerIdsForCg(localCgId, {
5257
+ const curatorResolution = await phases.measure('roster', () => this.resolveCuratorPeerIdsForCg(localCgId, {
4893
5258
  maxPeerIds: DKGAgentBase.VM_RECONCILE_EXACT_ROSTER_MAX,
4894
5259
  // Once overflow is proven, expose exactly one new ordered peer per pass.
4895
5260
  // One target can spend only one peer attempt, so advancing by more would
@@ -4904,7 +5269,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
4904
5269
  curatorIsLocal: false,
4905
5270
  legacyTripleResolved: false,
4906
5271
  lookupFailed: true,
4907
- }));
5272
+ })));
4908
5273
  if (!isRecoveryCurrent())
4909
5274
  return staleRecovery();
4910
5275
  const allResolvedCuratorPeerIds = [...new Set(curatorResolution.peerIds
@@ -4950,7 +5315,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
4950
5315
  let legacyPreferredPeerId;
4951
5316
  if (resolutionSucceeded && !curatorResolution.curatorIsLocal
4952
5317
  && resolvedCuratorPeerIds.length === 0) {
4953
- legacyPreferredPeerId = await this.resolvePreferredSyncPeerId(localCgId);
5318
+ legacyPreferredPeerId = await phases.measure('roster', () => this.resolvePreferredSyncPeerId(localCgId));
4954
5319
  }
4955
5320
  if (!isRecoveryCurrent())
4956
5321
  return staleRecovery();
@@ -4986,9 +5351,18 @@ export class SwmHostModeMethods extends DKGAgentBase {
4986
5351
  this.vmReconcileCuratorPageCursorByCg.delete(localCgId);
4987
5352
  this.vmReconcileCuratorPageCursorByCg.set(localCgId, legacyPagination.nextPageAfterPeerId);
4988
5353
  }
4989
- if (!curatorResolution.curatorIsLocal && curatorPeerIds.length > 0) {
4990
- this.vmReconcileCuratorPeersByCg.delete(localCgId);
4991
- this.vmReconcileCuratorPeersByCg.set(localCgId, curatorPeerIds);
5354
+ if (!curatorResolution.curatorIsLocal) {
5355
+ if (curatorPeerIds.length > 0) {
5356
+ this.vmReconcileCuratorPeersByCg.delete(localCgId);
5357
+ this.vmReconcileCuratorPeersByCg.set(localCgId, curatorPeerIds);
5358
+ }
5359
+ else {
5360
+ // Without the durable phonebook an empty curator tier usually means
5361
+ // the owner's profile was never fetched, and this pass can only ask
5362
+ // peers it is already connected to. One bounded phonebook fetch serves
5363
+ // every graph; its completion re-schedules this graph's recovery.
5364
+ this.requestOnDemandAgentsPhonebook(localCgId, 'vm-reconcile');
5365
+ }
4992
5366
  }
4993
5367
  this.pruneVmReconcileState();
4994
5368
  const connectedByPeerId = new Map(this.node.libp2p.getConnections()
@@ -4997,6 +5371,79 @@ export class SwmHostModeMethods extends DKGAgentBase {
4997
5371
  // Curator resolution may connect a missing peer, but it must not substitute
4998
5372
  // a different ranking algorithm and invalidate an otherwise stable roster.
4999
5373
  const orderedPeerIds = this.vmReconcileObservedCandidatePeerIds(localCgId);
5374
+ // Only an explicit experimental run may reorder these existing capped
5375
+ // transport candidates. The proof roster and every target's independent
5376
+ // attempted/absence credits stay unchanged. Identify does not prove data.
5377
+ const experimentalStreamPeerIds = new Set();
5378
+ // Observation only: why this pass did or did not choose the stream wire.
5379
+ let streamAdvertisedCount = 0;
5380
+ const advertisedStreamPeers = new Set();
5381
+ // The registered-public answer that gates the stream wire. Retry eligibility, the wire
5382
+ // choice and the log label all derive from this one typed observation.
5383
+ const passAuthority = new VmRecoveryPassAuthority(undefined, undefined, experimentPolicy.authorityRetry);
5384
+ const readRegisteredPublicAuthority = async () => {
5385
+ await phases.measure('authority', () => passAuthority.read(() => this.resolveRegisteredContextGraphAuthority(localCgId, {
5386
+ authorityReadMode: 'finalized-index-or-live', signal,
5387
+ })));
5388
+ if (passAuthority.isPublic && isRecoveryCurrent()) {
5389
+ for (const advertisedPeerId of advertisedStreamPeers)
5390
+ experimentalStreamPeerIds.add(advertisedPeerId);
5391
+ }
5392
+ };
5393
+ const prepareThisPassTargets = () => {
5394
+ if (!experimentPolicy.prefetchEnabled || orderedPeerIds.length === 0)
5395
+ return;
5396
+ // The planner sizes this pass's own targets as soon as a provider is chosen.
5397
+ // Start that early, but never ahead of the registered-authority read that gates
5398
+ // the wire: a miss there costs far more than the sizing it would overlap. The
5399
+ // first target keeps its own live read (a probe sizes its single asset itself),
5400
+ // so preparing never adds a read the unprepared path would not make. Pure
5401
+ // metadata: nothing is marked handled or attempted.
5402
+ transportPreparation?.preparePass(currentTargets
5403
+ .slice(1)
5404
+ .map((candidate) => ({ kaId: candidate.kaId })));
5405
+ };
5406
+ if (resolveExactBatchStreamEnabled()) {
5407
+ const transportInterval = phases.begin();
5408
+ const advertised = await Promise.all(orderedPeerIds
5409
+ .filter((peerId) => connectedByPeerId.has(peerId)
5410
+ && this.peerCapabilityRegistry.supportsCore(peerId)
5411
+ && !exactBatchStreamUnsupported(this, peerId, this.getSyncReconcilerConnectionKey(peerId), Date.now(), this.captureExperimentalExactBatchRefusalScope(localCgId)))
5412
+ .map(async (peerId) => {
5413
+ try {
5414
+ return (await this.getPeerProtocols(peerId)).includes(EXACT_BATCH_STREAM_PROTOCOL) ? peerId : undefined;
5415
+ }
5416
+ catch {
5417
+ return undefined;
5418
+ }
5419
+ }));
5420
+ phases.chargeSince('transport', transportInterval.startedAt, transportInterval.before);
5421
+ streamAdvertisedCount = advertised.filter((peerId) => peerId !== undefined).length;
5422
+ if (streamAdvertisedCount > 0) {
5423
+ for (const advertisedPeerId of advertised) {
5424
+ if (advertisedPeerId !== undefined)
5425
+ advertisedStreamPeers.add(advertisedPeerId);
5426
+ }
5427
+ await readRegisteredPublicAuthority();
5428
+ if (!isRecoveryCurrent())
5429
+ return staleRecovery();
5430
+ prepareThisPassTargets();
5431
+ if (experimentPolicy.authorityRetry.kind === 'spaced' && passAuthority.missed) {
5432
+ // A miss here is usually the shared local request budget, not the graph: the
5433
+ // read's detached resolution keeps warming the projection cache after its
5434
+ // deadline. Give it a short, bounded moment and ask once more before the
5435
+ // pass commits to the legacy singleton wire for every provider it tries.
5436
+ const retry = await phases.measure('authority', () => passAuthority.waitForMissRetry(signal));
5437
+ if (!isRecoveryCurrent())
5438
+ return staleRecovery();
5439
+ if (retry)
5440
+ await readRegisteredPublicAuthority();
5441
+ if (!isRecoveryCurrent())
5442
+ return staleRecovery();
5443
+ }
5444
+ }
5445
+ }
5446
+ prepareThisPassTargets();
5000
5447
  // Curator preparation may have grown or shrunk the connected candidate
5001
5448
  // set. Re-evaluate every target against that observed change. Any roster
5002
5449
  // change breaks backoff and starts a fresh proof cycle.
@@ -5062,6 +5509,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5062
5509
  const providerPolicy = new VmRecoveryProviderPolicy();
5063
5510
  const handledBatchOrdinals = new Set();
5064
5511
  let recoveryWorkRan = false;
5512
+ let localAdmissionDeferred = false;
5065
5513
  for (let eligibleIndex = 0; eligibleIndex < eligible.length; eligibleIndex += 1) {
5066
5514
  if (!isRecoveryCurrent())
5067
5515
  break;
@@ -5069,6 +5517,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5069
5517
  const { target } = entry;
5070
5518
  if (handledBatchOrdinals.has(target.ordinal))
5071
5519
  continue;
5520
+ const batchTiming = timing.beginBatch();
5072
5521
  const record = entry.prepared.record;
5073
5522
  const installedRecord = record
5074
5523
  && this.vmReconcileRotationState.get(this.vmReconcileRotationSlotKey(target)) === record
@@ -5083,21 +5532,27 @@ export class SwmHostModeMethods extends DKGAgentBase {
5083
5532
  // uncredited, but consume this target's turn so another missing KA gets
5084
5533
  // the next physical peer slot in the same bounded pass.
5085
5534
  let peerId;
5086
- const candidatePeerId = this.selectVmReconcileExactCandidate(installedRecord, orderedPeerIds, providerPolicy);
5535
+ if (advertisedStreamPeers.size > 0
5536
+ && passAuthority.retryDue()) {
5537
+ // One pass can try several providers over minutes, and every wire choice below
5538
+ // follows this single registered-public observation. A transient miss at pass
5539
+ // start (the read shares a saturated local request budget; its detached
5540
+ // resolution keeps warming the projection cache) would otherwise pin every
5541
+ // later provider of the pass to the legacy singleton wire. Read it again, at
5542
+ // most once per interval, before choosing the next provider.
5543
+ await readRegisteredPublicAuthority();
5544
+ if (!isRecoveryCurrent())
5545
+ return staleRecovery();
5546
+ }
5547
+ const candidatePeerId = this.selectVmReconcileExactCandidate(installedRecord, orderedPeerIds, providerPolicy, { localCgId, onChainCgId: expectedOnChainCgId, experimentalStreamPeerIds });
5548
+ // One policy capture owns every later transition and token fence.
5549
+ const preferenceAttempt = this.vmReconcilePublicCoreTransportPreferencePolicy.capture(localCgId, expectedOnChainCgId, orderedPeerIds);
5087
5550
  if (candidatePeerId) {
5088
- attemptedOrdinals.add(target.ordinal);
5089
- if (installedRecord) {
5090
- installedRecord.lastAttemptedPeerId = candidatePeerId;
5091
- installedRecord.attemptedPeerIds.add(candidatePeerId);
5092
- installedRecord.collectionDeadlineAt = this.vmReconcileRotationNow()
5093
- + DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_MAX_MS;
5094
- this.touchVmReconcileRotationRecord(this.vmReconcileRotationSlotKey(target), installedRecord);
5095
- }
5096
5551
  let connectedPeer = connectedByPeerId.get(candidatePeerId);
5097
5552
  if (!connectedPeer) {
5098
- await this.ensurePeerConnected(candidatePeerId, { signal }).catch((error) => {
5553
+ await phases.measure('peer-ready', () => this.ensurePeerConnected(candidatePeerId, { signal }).catch((error) => {
5099
5554
  this.log.info(ctx, `VM exact fetch could not connect candidate peer ${candidatePeerId.slice(-8)}: ${error instanceof Error ? error.message : String(error)}`);
5100
- });
5555
+ }));
5101
5556
  if (!isRecoveryCurrent())
5102
5557
  return staleRecovery();
5103
5558
  const connection = this.node.libp2p.getConnections()
@@ -5108,7 +5563,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5108
5563
  }
5109
5564
  recoveryWorkRan = true;
5110
5565
  const protocolReady = connectedPeer
5111
- ? await this.waitForSyncProtocol(connectedPeer, signal)
5566
+ ? await phases.measure('peer-ready', () => this.waitForSyncProtocol(connectedPeer, signal))
5112
5567
  : false;
5113
5568
  if (!isRecoveryCurrent())
5114
5569
  return staleRecovery();
@@ -5119,7 +5574,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5119
5574
  // Network boundary: a merely-connected peer is not necessarily
5120
5575
  // admitted to this DKG network. Never send an authenticated exact
5121
5576
  // request to an unverified or rejected peer.
5122
- const peerAdmitted = await this.ensurePeerAdmittedForRecovery(candidatePeerId, ctx, 'VM exact fetch', signal);
5577
+ const peerAdmitted = await phases.measure('peer-ready', () => this.ensurePeerAdmittedForRecovery(candidatePeerId, ctx, 'VM exact fetch', signal));
5123
5578
  if (!isRecoveryCurrent())
5124
5579
  return staleRecovery();
5125
5580
  if (!peerAdmitted)
@@ -5129,25 +5584,41 @@ export class SwmHostModeMethods extends DKGAgentBase {
5129
5584
  }
5130
5585
  }
5131
5586
  if (!peerId) {
5587
+ if (candidatePeerId) {
5588
+ this.vmReconcilePublicCoreTransportPreferencePolicy.revoke(preferenceAttempt, candidatePeerId);
5589
+ }
5590
+ // A failed connection/protocol/network-admission probe still consumes
5591
+ // its bounded provider turn. A ready peer is marked only by the exact
5592
+ // executor after local sync capacity admits its physical work.
5593
+ if (candidatePeerId) {
5594
+ attemptedOrdinals.add(target.ordinal);
5595
+ if (installedRecord) {
5596
+ installedRecord.lastAttemptedPeerId = candidatePeerId;
5597
+ installedRecord.attemptedPeerIds.add(candidatePeerId);
5598
+ installedRecord.collectionDeadlineAt = this.vmReconcileRotationNow()
5599
+ + DKGAgentBase.VM_RECONCILE_NEGATIVE_BACKOFF_MAX_MS;
5600
+ this.touchVmReconcileRotationRecord(this.vmReconcileRotationSlotKey(target), installedRecord);
5601
+ }
5602
+ }
5132
5603
  if (installedRecord) {
5133
5604
  this.settleVmReconcileRotationAttempt(target, undefined, 'incomplete', candidatePeerIds, installedRecord, providerPolicy.unavailablePeerIds());
5134
5605
  }
5135
5606
  continue;
5136
5607
  }
5608
+ const carriedHolder = this.vmReconcilePublicCoreTransportPreferencePolicy.canReuse(preferenceAttempt, peerId, candidatePeerIds);
5609
+ if (carriedHolder)
5610
+ providerPolicy.seedProvenHolder(peerId);
5137
5611
  const providerAttempt = providerPolicy.beginAttempt(peerId);
5138
5612
  if (!providerAttempt)
5139
5613
  continue;
5140
- let batchAttempts = [{
5141
- entry,
5142
- installedRecord,
5143
- candidatePeerIds,
5144
- }];
5145
- // The first exact request to a peer remains a single-KA probe. Once that
5146
- // probe has proved the peer is a holder, pack a stable compatible prefix
5147
- // by authoritative size hints. Unknown footprints stay singleton; the
5148
- // exact-sync protocol's ten-UAL cap remains the hard upper bound.
5149
- if (providerAttempt.kind === 'proven-holder-reuse') {
5150
- const compatible = [];
5614
+ const admittedConnectionKey = this.getSyncReconcilerConnectionKey(peerId);
5615
+ // Choose the admitted singleton probe or rotation-compatible holder
5616
+ // prefix before sizing; one pipeline owns its selection and wire mode.
5617
+ const candidateAttempts = [];
5618
+ if (providerAttempt.kind === 'probe') {
5619
+ candidateAttempts.push({ entry, installedRecord, candidatePeerIds });
5620
+ }
5621
+ else {
5151
5622
  for (let candidateIndex = eligibleIndex; candidateIndex < eligible.length; candidateIndex += 1) {
5152
5623
  const candidateEntry = eligible[candidateIndex];
5153
5624
  const candidateTarget = candidateEntry.target;
@@ -5158,8 +5629,8 @@ export class SwmHostModeMethods extends DKGAgentBase {
5158
5629
  && this.vmReconcileRotationState.get(this.vmReconcileRotationSlotKey(candidateTarget)) === candidateRecord
5159
5630
  ? candidateRecord
5160
5631
  : undefined;
5161
- // The current target was selected and marked attempted before the
5162
- // connection/admission boundary. Later candidates must still prove
5632
+ // The current target already passed connection/network admission.
5633
+ // Later candidates must still prove
5163
5634
  // this peer remains uncredited in their independent rotation record.
5164
5635
  const peerEligible = candidateIndex === eligibleIndex
5165
5636
  || (candidateInstalledRecord
@@ -5167,7 +5638,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5167
5638
  : orderedPeerIds.includes(peerId));
5168
5639
  if (!peerEligible)
5169
5640
  break;
5170
- compatible.push({
5641
+ candidateAttempts.push({
5171
5642
  entry: candidateEntry,
5172
5643
  installedRecord: candidateInstalledRecord,
5173
5644
  candidatePeerIds: candidateInstalledRecord
@@ -5175,53 +5646,85 @@ export class SwmHostModeMethods extends DKGAgentBase {
5175
5646
  : orderedPeerIds,
5176
5647
  });
5177
5648
  }
5178
- // Only candidates that survived rotation/admission are sized. Keeping
5179
- // this hint local to the planner avoids broadening the canonical chain
5180
- // ordinal target and does not waste bounded RPC reads on ineligible
5181
- // prefixes. Missing/failed hints remain conservative singletons.
5182
- const readVmRecoveryUpdateContext = this.chain.getKnowledgeAssetUpdateContext;
5183
- const sizedCandidates = await enrichVmRecoveryFootprints(compatible.map((attempt) => ({
5184
- attempt,
5185
- kaId: attempt.entry.target.kaId,
5186
- })), onChainCgId, {
5187
- resolvePublicAccess: async (contextGraphId) => (await withRpcUsageSite(CG_AUTH_RPC_SITES.vmSizing,
5188
- // Sizing a recovery batch: this decides how much to READ, not
5189
- // who may. A bound stale by one index tick changes a batch
5190
- // size, and the next pass corrects it. Inert unless the
5191
- // operator enables `chain.boundedAuthorityReads`.
5192
- () => this.readLiveOnChainAccessPolicy(contextGraphId.toString(), ctx, { freshness: 'bounded' }))) === 0,
5193
- sizing: typeof readVmRecoveryUpdateContext === 'function'
5194
- ? {
5195
- readUpdateContext: (kaId, readOptions) => readVmRecoveryUpdateContext.call(this.chain, kaId, readOptions),
5196
- }
5197
- : null,
5198
- }, {
5199
- maxContextReads: MAX_EXACT_SYNC_ASSETS,
5200
- signal,
5201
- isCurrent: isRecoveryCurrent,
5202
- });
5203
- if (!isRecoveryCurrent())
5204
- return noRecovery();
5205
- const plannableTargets = sizedCandidates.map(({ attempt, recoveryFootprint }) => ({
5206
- attempt,
5207
- recoveryFootprint,
5208
- }));
5209
- const plan = planVmRecoveryMicrobatch(plannableTargets, VM_EXACT_MICROBATCH_LIMITS, (plannedTargets) => Buffer.byteLength(encodeExactAssetUals(plannedTargets.map(({ attempt }) => attempt.entry.target.ual)), 'utf8'));
5210
- if (plan.targets.length === 0) {
5211
- this.log.warn(ctx, `VM exact recovery selector for "${localCgId}" exceeds the executor cap; `
5212
- + `ordinal=${target.ordinal} selectorCap=${VM_EXACT_MICROBATCH_LIMITS.maxSelectorBytes}`);
5213
- if (installedRecord) {
5214
- this.settleVmReconcileRotationAttempt(target, undefined, 'incomplete', candidatePeerIds, installedRecord, providerPolicy.unavailablePeerIds());
5215
- }
5216
- providerPolicy.finishAttempt(providerAttempt, 'incomplete', new Map([[target.ual, 'incomplete']]));
5217
- continue;
5649
+ }
5650
+ const streamEligibleProvider = passAuthority.isPublic && experimentalStreamPeerIds.has(peerId)
5651
+ && !exactBatchStreamUnsupported(this, peerId, admittedConnectionKey, Date.now(), this.captureExperimentalExactBatchRefusalScope(localCgId));
5652
+ let transportPlan;
5653
+ transportPlan = await phases.measure('sizing', () => planVmRecoveryTransport({
5654
+ candidates: candidateAttempts.map(attempt => ({
5655
+ attempt, kaId: attempt.entry.target.kaId, assetUal: attempt.entry.target.ual,
5656
+ })),
5657
+ providerAttemptKind: providerAttempt.kind, onChainCgId,
5658
+ streamEligible: streamEligibleProvider, registeredPublicAccess: passAuthority.isPublic,
5659
+ legacyAttemptTimeoutMs: this.vmReconcileTransportBudgetPolicy.timeoutFor({
5660
+ target, peerId, providerAttemptKind: providerAttempt.kind,
5661
+ registeredPublicAccess: passAuthority.isPublic,
5662
+ competingStreamAvailable: experimentalStreamPeerIds.size > 0
5663
+ && !experimentalStreamPeerIds.has(peerId),
5664
+ streamEligible: streamEligibleProvider,
5665
+ }),
5666
+ signal, isCurrent: isRecoveryCurrent,
5667
+ observeSizing: batchTiming.observeSizing,
5668
+ // Start reads in candidate order with a small bound so each read's
5669
+ // deadline measures its own round trip, not its wait in the local
5670
+ // RPC governor behind earlier candidates.
5671
+ ...experimentPolicy.sizing,
5672
+ ...(experimentPolicy.prefetchEnabled ? {
5673
+ probeRemainder: eligible.slice(eligibleIndex + 1)
5674
+ .filter(candidate => !handledBatchOrdinals.has(candidate.target.ordinal))
5675
+ .map(candidate => ({ kaId: candidate.target.kaId })),
5676
+ } : {}),
5677
+ }, {
5678
+ createSizingReader: () => {
5679
+ const readContext = this.chain.getKnowledgeAssetUpdateContext;
5680
+ return typeof readContext === 'function'
5681
+ ? { readUpdateContext: (kaId, readOptions) => readContext.call(this.chain, kaId, readOptions) }
5682
+ : null;
5683
+ },
5684
+ resolvePublicAccess: async (contextGraphId) => (await withRpcUsageSite(CG_AUTH_RPC_SITES.vmSizing,
5685
+ // This bounded observation controls soft sizing only. Canonical
5686
+ // root/version/binding authority is still checked per asset.
5687
+ () => this.readLiveOnChainAccessPolicy(contextGraphId.toString(), ctx, { freshness: 'bounded' }))) === 0,
5688
+ preparation: transportPreparation,
5689
+ }));
5690
+ if (!isRecoveryCurrent())
5691
+ return providerAttempt.kind === 'probe' ? staleRecovery() : noRecovery();
5692
+ const { attempts: batchAttempts, transportMode: exactRecoveryTransportMode, publicAccessEvidence: publicRecoveryAccessVerified, packing, legacyAttemptTimeoutMs, } = transportPlan;
5693
+ if (batchAttempts.length === 0) {
5694
+ this.vmReconcilePublicCoreTransportPreferencePolicy.revoke(preferenceAttempt, peerId);
5695
+ this.log.warn(ctx, `VM exact recovery selector for "${localCgId}" exceeds the executor cap; `
5696
+ + `ordinal=${target.ordinal} selectorCap=${VM_EXACT_MICROBATCH_LIMITS.maxSelectorBytes}`);
5697
+ if (installedRecord) {
5698
+ this.settleVmReconcileRotationAttempt(target, undefined, 'incomplete', candidatePeerIds, installedRecord, providerPolicy.unavailablePeerIds());
5218
5699
  }
5219
- batchAttempts = plan.targets.map(({ attempt }) => attempt);
5700
+ providerPolicy.finishAttempt(providerAttempt, 'incomplete', new Map([[target.ual, 'incomplete']]));
5701
+ continue;
5702
+ }
5703
+ observeVmRecoveryTiming(() => this.log.info(ctx, `VM exact recovery transport for "${localCgId}" from ${peerId.slice(-8)}: kind=${providerAttempt.kind} `
5704
+ + `transport=${exactRecoveryTransportMode} assets=${batchAttempts.length} `
5705
+ + `streamAdvertised=${streamAdvertisedCount} streamPeers=${experimentalStreamPeerIds.size} `
5706
+ + `registeredAuthority=${passAuthority.label}`));
5707
+ if (packing !== undefined) {
5220
5708
  this.log.info(ctx, `VM exact recovery plan for "${localCgId}" from ${peerId.slice(-8)}: `
5221
- + `assets=${batchAttempts.length} estimatedBytes=${plan.estimatedBytes} `
5222
- + `estimatedLeaves=${plan.estimatedLeaves} `
5223
- + `completeFootprints=${plan.completeFootprints}`);
5709
+ + `assets=${batchAttempts.length} estimatedBytes=${packing.estimatedBytes} `
5710
+ + `estimatedLeaves=${packing.estimatedLeaves} `
5711
+ + `completeFootprints=${packing.completeFootprints} transport=${exactRecoveryTransportMode}`);
5712
+ }
5713
+ if (carriedHolder && !this.vmReconcilePublicCoreTransportPreferencePolicy.revalidateReuse(preferenceAttempt, peerId, admittedConnectionKey)) {
5714
+ // Sizing outlived the captured holder proof; the next slice must probe.
5715
+ if (activeFetchCooldownOwner !== undefined) {
5716
+ this.clearVmReconcileActiveFetchCooldown(localCgId, activeFetchCooldownOwner);
5717
+ }
5718
+ return {
5719
+ outcomes, attemptedOrdinals: [...attemptedOrdinals],
5720
+ continuationOrdinal: target.ordinal, hasImmediateRecoveryWork: true, cooldownOnly: false,
5721
+ };
5224
5722
  }
5723
+ // Only while preparation is on: the exchange may rely on this pass's own fresh positive
5724
+ // answer instead of reading it again. The handle dies with the exchange.
5725
+ const registeredPublicEvidence = experimentPolicy.sharePassAuthorityEvidence
5726
+ ? passAuthority.evidence({ contextGraphId: localCgId, signal, isCurrent: isRecoveryCurrent })
5727
+ : undefined;
5225
5728
  const execution = await this.executeVmRecoveryBatch({
5226
5729
  localCgId,
5227
5730
  onChainCgId,
@@ -5233,16 +5736,52 @@ export class SwmHostModeMethods extends DKGAgentBase {
5233
5736
  isRecoveryCurrent,
5234
5737
  revalidateTarget,
5235
5738
  ctx,
5236
- });
5237
- if (execution.kind !== 'completed')
5739
+ exactRecoveryTransportMode,
5740
+ ...(legacyAttemptTimeoutMs === undefined ? {} : { legacyAttemptTimeoutMs }),
5741
+ ...(registeredPublicEvidence ? { registeredPublicEvidence } : {}),
5742
+ phases,
5743
+ }).finally(() => registeredPublicEvidence?.revoke());
5744
+ if (execution.kind === 'local-admission-deferred') {
5745
+ // Capacity is node-local, so trying other providers would burn their
5746
+ // turns under the same refusal. Retain all peer evidence and yield the
5747
+ // whole slice for the scheduler's bounded local retry.
5748
+ localAdmissionDeferred = true;
5749
+ break;
5750
+ }
5751
+ if (execution.kind !== 'completed' || !isRecoveryCurrent())
5238
5752
  return staleRecovery();
5753
+ batchTiming.complete({ peerId, assets: batchAttempts.length,
5754
+ candidates: eligible.length - eligibleIndex, kind: providerAttempt.kind,
5755
+ transport: exactRecoveryTransportMode, streamAdvertised: streamAdvertisedCount,
5756
+ streamPeers: experimentalStreamPeerIds.size, registeredAuthority: passAuthority.label });
5239
5757
  for (const [ordinal, outcome] of execution.outcomes)
5240
5758
  outcomes.set(ordinal, outcome);
5241
5759
  for (const ordinal of execution.handledOrdinals)
5242
5760
  handledBatchOrdinals.add(ordinal);
5243
5761
  for (const ordinal of execution.attemptedOrdinals)
5244
5762
  attemptedOrdinals.add(ordinal);
5245
- providerPolicy.finishAttempt(providerAttempt, execution.providerDisposition, new Map(execution.perUalDispositions));
5763
+ if (execution.providerTurnKept) {
5764
+ // Busy or a broken stream: the peer is not spent for this slice. It is
5765
+ // asked again, with a fresh probe, once its hold-off has passed.
5766
+ providerPolicy.releaseAttempt(providerAttempt);
5767
+ }
5768
+ else {
5769
+ providerPolicy.finishAttempt(providerAttempt, execution.providerDisposition, new Map(execution.perUalDispositions));
5770
+ }
5771
+ const completelyVerified = execution.providerDisposition === 'found'
5772
+ && execution.perUalDispositions.length === batchAttempts.length
5773
+ && execution.perUalDispositions.every(([, disposition]) => disposition === 'found');
5774
+ const preferenceSettlement = this.vmReconcilePublicCoreTransportPreferencePolicy.settle(preferenceAttempt, {
5775
+ peerId, connectionKey: admittedConnectionKey, completelyVerified,
5776
+ publicAccessVerified: publicRecoveryAccessVerified,
5777
+ reusedHolder: providerAttempt.kind === 'proven-holder-reuse', carriedHolder,
5778
+ });
5779
+ if (preferenceSettlement.yield) {
5780
+ this.log.info(ctx, `VM exact recovery yield: reason=verified-core-turn assets=${batchAttempts.length} `
5781
+ + `publicSizingAccess=${publicRecoveryAccessVerified === undefined ? 'unknown' : publicRecoveryAccessVerified} `
5782
+ + `holderCreditPresent=${preferenceSettlement.holderCreditPresent}`);
5783
+ break;
5784
+ }
5246
5785
  }
5247
5786
  const eligibleOrdinals = new Set(eligible.map(({ target }) => target.ordinal));
5248
5787
  const unattemptedContinuationOrdinal = currentTargets.find((target) => eligibleOrdinals.has(target.ordinal) && !attemptedOrdinals.has(target.ordinal))?.ordinal;
@@ -5273,6 +5812,19 @@ export class SwmHostModeMethods extends DKGAgentBase {
5273
5812
  // negative backoff applies exactly as before.
5274
5813
  if (!isRecoveryCurrent())
5275
5814
  return staleRecovery();
5815
+ if (localAdmissionDeferred) {
5816
+ if (activeFetchCooldownOwner !== undefined) {
5817
+ this.clearVmReconcileActiveFetchCooldown(localCgId, activeFetchCooldownOwner);
5818
+ }
5819
+ return {
5820
+ outcomes,
5821
+ attemptedOrdinals: [...attemptedOrdinals],
5822
+ continuationOrdinal: unattemptedContinuationOrdinal,
5823
+ hasImmediateRecoveryWork: false,
5824
+ cooldownOnly: false,
5825
+ localAdmissionDeferred: true,
5826
+ };
5827
+ }
5276
5828
  const recoveredAny = [...outcomes.values()]
5277
5829
  .some((outcome) => outcome.status === 'reconciled' || outcome.status === 'already');
5278
5830
  if (!recoveryWorkRan
@@ -5289,6 +5841,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5289
5841
  this.installVmReconcileActiveFetchCooldown(localCgId, Date.now());
5290
5842
  }
5291
5843
  }
5844
+ timing.finish(eligible.length, preparation ? () => preparation.stats() : undefined);
5292
5845
  return {
5293
5846
  outcomes,
5294
5847
  attemptedOrdinals: [...attemptedOrdinals],
@@ -5306,7 +5859,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5306
5859
  * latest on-chain merkle root + publisher, build the UAL, and ask the
5307
5860
  * finalization handler to promote the matching local SWM snapshot to VM
5308
5861
  * (verifying the CG binding from chain). When no local SWM matches, run an
5309
- * active core-first catch-up fetch and retry once. A successful result is
5862
+ * queue the exact target for bounded batch recovery. A successful result is
5310
5863
  * validated against one coherent pinned version snapshot before it earns a
5311
5864
  * same-finalized-block shortcut; a new block always reads again. `headBlock`
5312
5865
  * remains the independent cursor observation for the reorg-depth gate. See
@@ -5314,7 +5867,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5314
5867
  *
5315
5868
  * Local first: before any root, publisher or version read, the handler
5316
5869
  * classifies what the store holds for the KA. A confirmed VM copy with
5317
- * nothing else to promote settles as `already`, and in the deferred mode an
5870
+ * nothing else to promote settles as `already`, and an
5318
5871
  * asset held nowhere locally goes straight to the exact-recovery queue, both
5319
5872
  * without chain reads. Only local state whose outcome depends on the root
5320
5873
  * takes the chain-backed path below.
@@ -5375,8 +5928,10 @@ export class SwmHostModeMethods extends DKGAgentBase {
5375
5928
  }, ctx)
5376
5929
  : { kind: 'present' };
5377
5930
  if (localCandidate.kind === 'confirmed-vm') {
5378
- // Trusted at the root the copy was confirmed at; see the classifier for
5379
- // how an update to it still reaches VM.
5931
+ // Trusted at the root the copy was confirmed at, with no chain read. An
5932
+ // update to it reaches VM through the `KnowledgeAssetUpdated` refresh
5933
+ // (`handleKAUpdatedNudge`), not through this walk: a settled ordinal is
5934
+ // never visited again.
5380
5935
  if (!(await targetMayMaterialize()))
5381
5936
  return { status: 'skip' };
5382
5937
  this.clearVmReconcileRotationStateForSlot(localCgId, onChainCgId, ordinal);
@@ -5386,11 +5941,10 @@ export class SwmHostModeMethods extends DKGAgentBase {
5386
5941
  });
5387
5942
  return { status: 'already', blockNumber: headBlock ?? 0 };
5388
5943
  }
5389
- if (localCandidate.kind === 'none' && options.deferActiveFetch) {
5944
+ if (localCandidate.kind === 'none') {
5390
5945
  // Nothing local can match any root, so the chain-backed path could only
5391
5946
  // answer `no-swm`. Queue the same exact-recovery target it would; the
5392
5947
  // recovery batch keeps its rotation backoff, cooldown and peer gating.
5393
- // The inline mode keys its negative cache by the root and keeps reading it.
5394
5948
  if (!(await targetMayMaterialize()))
5395
5949
  return { status: 'skip' };
5396
5950
  this.emitReplication({
@@ -5452,18 +6006,6 @@ export class SwmHostModeMethods extends DKGAgentBase {
5452
6006
  }
5453
6007
  merkleRoot = await this.chain.getLatestMerkleRoot(kaId);
5454
6008
  cacheKey = this.vmReconcileCacheKey(localCgId, ual, merkleRoot);
5455
- if (!options.deferActiveFetch && await this.shouldDeferVmReconcileByNegativeCache(cacheKey, localCgId)) {
5456
- this.emitReplication({
5457
- contextGraphId: localCgId,
5458
- onChainCgId: onChainCgId.toString(),
5459
- action: 'defer',
5460
- ordinal,
5461
- kaId: kaId.toString(),
5462
- ual,
5463
- detail: 'negative-cache',
5464
- });
5465
- return { status: 'pending' };
5466
- }
5467
6009
  publisherAddress = (this.chain.getLatestMerkleRootPublisher
5468
6010
  ? await this.chain.getLatestMerkleRootPublisher(kaId)
5469
6011
  : '') ?? '';
@@ -5510,119 +6052,33 @@ export class SwmHostModeMethods extends DKGAgentBase {
5510
6052
  return undefined;
5511
6053
  }
5512
6054
  };
5513
- let swmState;
5514
- let activeFetchRan = false;
5515
- let activeFetchHadUsableResponse = false;
5516
- const cleanMissPeerIds = new Set();
5517
6055
  if (!(await targetMayMaterialize()))
5518
6056
  return { status: 'skip' };
5519
- let outcome = await reconcileKnowledgeAsset();
6057
+ const outcome = await reconcileKnowledgeAsset();
5520
6058
  if (outcome === undefined)
5521
6059
  return { status: 'pending' };
5522
6060
  if (outcome === 'no-swm' || outcome === 'verified-vm-metadata-pending') {
5523
- if (options.deferActiveFetch) {
5524
- this.emitReplication({
5525
- contextGraphId: localCgId,
6061
+ this.emitReplication({
6062
+ contextGraphId: localCgId,
6063
+ onChainCgId: onChainCgId.toString(),
6064
+ action: 'defer',
6065
+ ordinal,
6066
+ kaId: kaId.toString(),
6067
+ ual,
6068
+ detail: outcome,
6069
+ });
6070
+ return {
6071
+ status: 'pending',
6072
+ recovery: {
6073
+ localCgId,
5526
6074
  onChainCgId: onChainCgId.toString(),
5527
- action: 'defer',
5528
6075
  ordinal,
5529
- kaId: kaId.toString(),
5530
6076
  ual,
5531
- detail: outcome,
5532
- });
5533
- return {
5534
- status: 'pending',
5535
- recovery: {
5536
- localCgId,
5537
- onChainCgId: onChainCgId.toString(),
5538
- ordinal,
5539
- ual,
5540
- merkleRoot: Array.from(merkleRoot, (byte) => byte.toString(16).padStart(2, '0')).join(''),
5541
- kaId: kaId.toString(),
5542
- reason: outcome,
5543
- },
5544
- };
5545
- }
5546
- if (outcome === 'no-swm') {
5547
- swmState = await this.collectVmReconcileSwmCandidateState(localCgId);
5548
- }
5549
- // Active fetch: pull the missing snapshot core-first (selectCatchupPeers
5550
- // already prioritises known cores + the preferred sync peer), then retry.
5551
- // Metadata-pending exact VM content needs the same recovery: a durable
5552
- // sync can supply the missing provenance-bearing assertion metadata even
5553
- // when no content triples need to move.
5554
- const batchAllowsFetch = options.acquireActiveFetchPermit?.() ?? true;
5555
- const cooldownAllowsFetch = batchAllowsFetch
5556
- && this.shouldRunVmReconcileActiveFetch(localCgId);
5557
- if (batchAllowsFetch && cooldownAllowsFetch) {
5558
- activeFetchRan = true;
5559
- this.emitReplication({
5560
- contextGraphId: localCgId, onChainCgId: onChainCgId.toString(),
5561
- action: 'fetch', ordinal, kaId: kaId.toString(), ual,
5562
- });
5563
- let maxAttempts = 1;
5564
- const fixedMaxAttempts = options.maxPeerAttempts === undefined
5565
- ? undefined
5566
- : Math.max(1, Math.floor(options.maxPeerAttempts));
5567
- if (fixedMaxAttempts !== undefined)
5568
- maxAttempts = fixedMaxAttempts;
5569
- for (let attempt = 0; attempt < maxAttempts
5570
- && (outcome === 'no-swm' || outcome === 'verified-vm-metadata-pending'); attempt += 1) {
5571
- if (options.isTargetCurrent && !options.isTargetCurrent()) {
5572
- break;
5573
- }
5574
- try {
5575
- const recovery = await this.syncVmRecoveryFromConnectedPeers(localCgId, {
5576
- includeSharedMemory: true,
5577
- maxPeers: 1,
5578
- peerRotationKey: localCgId,
5579
- // This is recovery, not routine background catch-up. Without the
5580
- // override it would enter through the default-background path and
5581
- // be reported as `catchup-background`, merging repair traffic into
5582
- // the background lane. Attribution only — mode, priority, peer
5583
- // selection and the coalescing key are all unchanged.
5584
- sourceOverride: 'vm-recovery',
5585
- });
5586
- const fetchResult = recovery.catchup;
5587
- if (fixedMaxAttempts === undefined) {
5588
- maxAttempts = Math.max(maxAttempts, fetchResult.totalPeers ?? fetchResult.connectedPeers ?? 0, this.vmReconcileConnectedPeerCount());
5589
- }
5590
- if ((fetchResult.peersTried ?? 0) === 0 && (fetchResult.syncCapablePeers ?? 0) === 0) {
5591
- continue;
5592
- }
5593
- if (!this.vmReconcileActiveFetchHadUsableResponse(fetchResult)) {
5594
- continue;
5595
- }
5596
- activeFetchHadUsableResponse = true;
5597
- for (const peerId of recovery.cleanMissPeerIds) {
5598
- cleanMissPeerIds.add(peerId);
5599
- }
5600
- }
5601
- catch (err) {
5602
- this.log.info(ctx, `Phase B: active fetch for "${localCgId}" (ordinal ${ordinal}) failed: ${err instanceof Error ? err.message : String(err)}`);
5603
- if (fixedMaxAttempts === undefined) {
5604
- maxAttempts = Math.max(maxAttempts, this.vmReconcileConnectedPeerCount());
5605
- }
5606
- continue;
5607
- }
5608
- if (options.isTargetCurrent && !options.isTargetCurrent()) {
5609
- break;
5610
- }
5611
- if (!(await targetMayMaterialize()))
5612
- return { status: 'skip' };
5613
- outcome = await reconcileKnowledgeAsset();
5614
- if (outcome === undefined)
5615
- return { status: 'pending' };
5616
- }
5617
- if (outcome === 'no-swm') {
5618
- swmState = await this.collectVmReconcileSwmCandidateState(localCgId);
5619
- swmState.cleanMissPeerIds = createVmReconcileCleanMissPeerIds(swmState.peerTopology, [...cleanMissPeerIds]);
5620
- }
5621
- }
5622
- else {
5623
- const reason = batchAllowsFetch ? 'per-CG cooldown' : 'per-batch fetch budget';
5624
- this.log.info(ctx, `Phase B: active fetch for "${localCgId}" (ordinal ${ordinal}) skipped by ${reason}`);
5625
- }
6077
+ merkleRoot: Array.from(merkleRoot, (byte) => byte.toString(16).padStart(2, '0')).join(''),
6078
+ kaId: kaId.toString(),
6079
+ reason: outcome,
6080
+ },
6081
+ };
5626
6082
  }
5627
6083
  if (options.isTargetCurrent && !options.isTargetCurrent()) {
5628
6084
  return { status: 'skip' };
@@ -5638,7 +6094,6 @@ export class SwmHostModeMethods extends DKGAgentBase {
5638
6094
  case 'promoted':
5639
6095
  this.clearVmReconcileRotationStateForSlot(localCgId, onChainCgId, ordinal);
5640
6096
  this.pruneVmReconcileCacheKeySiblings(cacheKey);
5641
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
5642
6097
  this.recentReconciledUals.add(cacheKey);
5643
6098
  await this.confirmAndRememberVmReconcileFinalizedSlot(localCgId, onChainCgId, ordinal, rememberSlotBlock, kaId, merkleRoot, publisherAddress);
5644
6099
  this.emitReplication({
@@ -5655,7 +6110,6 @@ export class SwmHostModeMethods extends DKGAgentBase {
5655
6110
  contextGraphId: localCgId, onChainCgId: onChainCgId.toString(),
5656
6111
  action: 'already', ordinal, kaId: kaId.toString(), ual,
5657
6112
  });
5658
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
5659
6113
  return { status: 'already', blockNumber: completionBlock };
5660
6114
  case 'stale-target':
5661
6115
  this.clearVmReconcileRotationStateForSlot(localCgId, onChainCgId, ordinal);
@@ -5666,20 +6120,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5666
6120
  contextGraphId: localCgId, onChainCgId: onChainCgId.toString(),
5667
6121
  action: 'already', ordinal, kaId: kaId.toString(), ual,
5668
6122
  });
5669
- this.deleteVmReconcileNegativeCacheEntry(cacheKey);
5670
6123
  return { status: 'already', blockNumber: completionBlock };
5671
- case 'no-swm':
5672
- if (activeFetchRan && !activeFetchHadUsableResponse) {
5673
- this.clearVmReconcileActiveFetchCooldown(localCgId);
5674
- }
5675
- else {
5676
- this.recordVmReconcileNegativeCache(cacheKey, localCgId, swmState ?? await this.collectVmReconcileSwmCandidateState(localCgId));
5677
- }
5678
- this.emitReplication({
5679
- contextGraphId: localCgId, onChainCgId: onChainCgId.toString(),
5680
- action: 'defer', ordinal, kaId: kaId.toString(), ual, detail: activeFetchRan && !activeFetchHadUsableResponse ? 'network-unavailable' : outcome,
5681
- });
5682
- return { status: 'pending' };
5683
6124
  case 'receipt-revalidation-pending':
5684
6125
  case 'unverified':
5685
6126
  default:
@@ -5899,7 +6340,7 @@ export class SwmHostModeMethods extends DKGAgentBase {
5899
6340
  // on nodes that have persisted the CG's `allowedPeers`; host-only
5900
6341
  // cores never see it.
5901
6342
  try {
5902
- const allowedPeers = await this.getContextGraphAllowedPeers(req.contextGraphId);
6343
+ const allowedPeers = await this.resolveSwmAllowedPeersForCurrentAuthority(req.contextGraphId);
5903
6344
  if (allowedPeers !== null) {
5904
6345
  anyAuthoritySourceFound = true;
5905
6346
  if (allowedPeers.includes(fromPeerId)) {
@@ -6229,6 +6670,9 @@ export class SwmHostModeMethods extends DKGAgentBase {
6229
6670
  * two would race / duplicate every apply (Codex PR #610 R4).
6230
6671
  */
6231
6672
  async enableSwmHostModeFor(contextGraphId) {
6673
+ const session = this.gossipSession;
6674
+ if (!session.active)
6675
+ return { subscribed: false, alreadySubscribed: false, hostingEnabled: false };
6232
6676
  if (!this.swmHostModeStore) {
6233
6677
  return { subscribed: false, alreadySubscribed: false, hostingEnabled: false };
6234
6678
  }
@@ -6252,6 +6696,9 @@ export class SwmHostModeMethods extends DKGAgentBase {
6252
6696
  }
6253
6697
  const hostKey = this.canonicalSwmHostModeKey(contextGraphId);
6254
6698
  const curated = await this.isCuratedForHostMode(contextGraphId);
6699
+ if (!session.active || this.gossipSession !== session) {
6700
+ return { subscribed: false, alreadySubscribed: false, hostingEnabled: true };
6701
+ }
6255
6702
  if (this.swmHostModeSubscribed.has(hostKey)) {
6256
6703
  // Idempotent re-entry: even when the subscription is already
6257
6704
  // active, re-probe registration state. This handles the