chainlesschain 0.162.198 → 0.162.200

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 (195) hide show
  1. package/bin/chainlesschain.js +14 -6
  2. package/package.json +4 -1
  3. package/scripts/command-lifecycle-report.mjs +94 -0
  4. package/src/assets/web-panel/assets/{AIOps-CSN-D2xb.js → AIOps-DcQ0bWKy.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-CBSMlGzv.js → ActionButton-GecwfFPt.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-3yr3Nt2t.js → Analytics-CD6iSt9v.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-1b-o3lMH.js → AppLayout-CDEtuEYf.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-Dr8rJi17.js → Artifacts-DS3jAjeW.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-2V689HC2.js → Audit-CcSJA048.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-DIxkNrD_.js → BackgroundAgents-DRo9Morb.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-Pg6BcwrE.js → Backup-Vw5MrWft.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-CLUpqyfH.js → BaseInput-DNHIu823.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-qVS8rD4G.js → Chat-DvAsO8-b.js} +5 -5
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-ti49X12Q.js → ChatBubbleRenderer-9aDuObay.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-Bs9AaEay.js → Checkbox-CKCa7tvn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-Dvc1mq8q.js → Codegen-CIFvZdaT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-C4_biO7A.js → Col-C79yz7bK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-al25XQUV.js → Community-DVvZskLJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-D-ZDB8M1.js → Compact-oaew61hV.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-6lIfDz1W.js → Compliance-DqA7wmRx.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DdI48XMJ.js → Cowork-B-qGadxa.js} +2 -2
  22. package/src/assets/web-panel/assets/{Cron-DF5HZd2g.js → Cron-CUlCl4sS.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-DuxeSvt3.js → Crosschain-CNOlr9DU.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-B9yHkUTd.js → DID-DJqFCT_3.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-Z5EuI3IM.js → Dashboard-CznGpphf.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-vuRfO3dY.js → Dropdown-_cJw218E.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-QFLCc1uL.js → EmailListRenderer-Dr-1BZ9C.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ7EXkFJ.js → FamilyGuardDashboard-Dxf4xtSk.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-DycIEg2s.js → Federation-DOiG3i_1.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-Ba0H_7yG.js → FormItemContext-BzbMcFB9.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer--JgSTK-C.js → GenericCardRenderer-VbBrYttP.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DEvTbldE.js → Git-CLG8E6ED.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-BJIljy18.js → Governance-LGJQfk_q.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-CVyW3G3q.js → Inference-DlQKx9lF.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-v8-borge.js → KnowledgeGraph-DU8vEouV.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BCdS7gGo.js → Logs-FZIQOdln.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BJNWIrbw.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-AN6tGA2B.js → Marketplace-C_xd-zZI.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-DGViXASN.js → McpTools-q-YktphS.js} +5 -5
  40. package/src/assets/web-panel/assets/{Memory-Bpvmx9X4.js → Memory-Cq9OhDin.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CTP-DOTC.js → MobileBridge-B03_R98T.js} +1 -1
  42. package/src/assets/web-panel/assets/{MobileProjects-C8qfktxn.js → MobileProjects-DZrk-qmF.js} +1 -1
  43. package/src/assets/web-panel/assets/{Mtc-ndYr0M1s.js → Mtc-BidCKW28.js} +2 -2
  44. package/src/assets/web-panel/assets/{MtcAudit-Ci0-YK7W.js → MtcAudit-r-iaMVM7.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-BehSTFW7.js → Multisig-CgTUFvff.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-B91_YBLv.js → NLProgramming-B7NvzfAV.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BQlJ0-gq.js → Notes-80CWNqJI.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-CDgYMjYK.js → NotificationSettings-0H6xdiY3.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-BJkSlKeH.js → OrderTableRenderer-CKP4AFWy.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-B-b3cWCp.js → Organization-B8sxCV_h.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-CILUL4nL.js → Overflow-CW9JBBhQ.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BTCFBkF0.js → P2P-bdSQV7oZ.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BAR6AUx5.js → PdhVaultBrowser-DQad42nW.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-RRf6HbWJ.js → Permissions-CueexxTR.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-BGTMiICa.js → PersonalDataHub-fqxflhR8.js} +6 -6
  56. package/src/assets/web-panel/assets/{Pipeline-945qY9YF.js → Pipeline-KFwwnIMC.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-DPZwI6lJ.js → Privacy-ClnD9buY.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-BPH2NRKc.js → ProjectInit-CZEz6sm3.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-BZCRNCvb.js → ProjectSettings-B6NzpmOI.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-BTI__tBt.js → Projects-DYyuwPCs.js} +1 -1
  61. package/src/assets/web-panel/assets/Providers-C3tz_jdh.js +1 -0
  62. package/src/assets/web-panel/assets/{QrScannerModal-N6rbqt1r.js → QrScannerModal-BzuOBbqB.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DrwjPS7M.js → QuickAsk-CmNO9qSj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cog9Wd99.js → Recommend-VJNj3_NF.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-DC9pynjM.js → RemoteSession-CrbdbSBF.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-D4th4gKS.js → Reputation-DTyMd45j.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-ogiWR6v9.js → Row-DHrQ-Icz.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-uYlk-2Oj.js → RssFeed-DqWxY9c5.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-DYzzwvtM.js → Search-ctF1Ks3J.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Da7YOyX6.js → Security-BrQz8uo5.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-CLI1VM1X.js → Services-D4scVFar.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DK77dzDC.js → Skeleton-oUScaOjv.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-C_z7Lmhl.js → Skills-CL-y0eLt.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-D6413Q8L.js → Sla-DQExhwXj.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-C07ydbj2.js → SpeechSettings-iNp_I7kN.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-DGOm2W22.js → SyncSettings-CTDlMo0A.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-Cr_PVyLM.js → Tasks-4kb8l3sc.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-BhL2mZ0t.js → Templates-BXcL9oXW.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-CPk5oyUr.js → Tenant-DLE8eS0D.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-ClhqUsqV.js → Terminal-DGpUZPPK.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-BCC7MV5a.js → TimelineRenderer-C1pWXGPC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-DD1Jt4oO.js → Tokens-BYfbfqhZ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BCydFvN2.js → Trigger-K9YfLOCw.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DDHT1noa.js → Trust-Ctkq77Hy.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-CWPZC7oI.js → UkeySign-C6_CSe9J.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-9Ea0nJR0.js → VideoEditing-Be3rpOWs.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-BjxhLxTf.js → Wallet-D0UIplxk.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-C6uCye67.js → WebAuthn-CF9bM9I3.js} +3 -3
  89. package/src/assets/web-panel/assets/{WorkflowEditor-d2g4reYo.js → WorkflowEditor-D0H1XVPY.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-frTc6ScX.js → chat-Cgzzbe0o.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-B5K6lG3v.js → colors-BwNxEVS0.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-Cdn46r8D.js → compact-item-D60Z4Hhs.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-DU1jqVQi.js → createContext-MFk58XB0.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-BJiw369i.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-DdNH-aW_.js → hasIn-CRj2SvGO.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DM5FmPZU.js → index--s45_1cb.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D4l82Me1.js → index--tquQ2ZG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CQorP1Ng.js → index-1T1rhokn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CHmkkdfT.js → index-B-M82_rx.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CGiwfwB9.js → index-BE_el1Zs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BwlIxAmX.js → index-BhRkR7IO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cj6P9Nst.js → index-Bo0y96-K.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BoFak7le.js → index-CJgu5JUK.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dca_i7H8.js → index-CLWwdebO.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BVLL-mq_.js → index-CNAK5QG_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-LaivuXyF.js → index-CQjJjPRD.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-t_OBzIP4.js → index-CTDbAfae.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BcGwGIaR.js → index-CVI_9sRK.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D23xGYjM.js → index-CY14oMQK.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-VDM2KKo9.js → index-CbiTJkZf.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CiMouosn.js +1 -0
  112. package/src/assets/web-panel/assets/{index-32ajXmTb.js → index-CqWpxMqx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-PjAS0sIZ.js → index-CvB8lYUR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-7ehytj1K.js → index-D51XwtCY.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DtBCCkC4.js → index-D6HvK_1O.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Be4OtL-N.js → index-D9jVIwLJ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CUoNAmE8.js → index-DA4tE98u.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-cYaWV4Mq.js → index-DBwOtXw0.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-MM4Zz2OE.js → index-DKdNzTIb.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DIVyTmZ3.js → index-DRySdmvN.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CtRPi4YC.js → index-DWzyTev7.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DXcKlwy7.js +1 -0
  123. package/src/assets/web-panel/assets/{index-kVO_MDm-.js → index-DYjs7jAP.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4miNkWv.js → index-DbROzMBp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-yZRuZpwK.js → index-DhFtk5PR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BXKhhx9V.js → index-Di2AbMLg.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BC4eGkar.js → index-DvmocQsM.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-NmEXVjzt.js → index-Dw-o0zEF.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DRzBBdQ0.js → index-DysPoP38.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-HjCvXHf5.js → index-LX9pJxm_.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-gg3QPC3i.js → index-OI_fXWx0.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-mgU3Q-lN.js → index-eDTOX8Xj.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-D1qIC4GR.js → index-gWKDsTzF.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DftZuyA7.js → index-q_GDUVAA.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-DwLTRNdo.js → initDefaultProps-C61kZqEd.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-BchZDXnh.js → motion-DZnT5U1L.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-e8M-zZFl.js → move-L8wzwbel.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-Ci40fQb3.js → mtc-parser-DLdYSejc.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-C7110_f2.js → omit-D53cbS59.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-skteMgnM.js → pickAttrs-Bbc5HPtu.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-MGZ-lh2B.js → placementArrow-BkvmO860.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-N4Sd7uy-.js → responsiveObserve-u2y1ryS1.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-C7HDGRaa.js → slide-BBqj7svU.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BhRexlgy.js → statusUtils-GN0mqu2d.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-ChEoQkN2.js → styleChecker-BvwPKoBT.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-CDbG1s2Y.js → useFlexGapSupport-CCt8tnfY.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUIx0tcJ.js → useFs-DXAxl2SV.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-bkO7L7ec.js → usePersonalDataHub-DihlR2dj.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-B_ZvABIG.js → vnode-D7BOX2gi.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-D5shjpS8.js → zoom-CVbO-nL7.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/mcp.js +119 -0
  154. package/src/commands/session-show.js +8 -10
  155. package/src/commands/session.js +60 -52
  156. package/src/data/changelog.json +27 -1
  157. package/src/gateways/ws/session-protocol.js +14 -4
  158. package/src/gateways/ws/ws-agent-handler.js +31 -0
  159. package/src/harness/jsonl-session-store.js +1147 -130
  160. package/src/harness/mcp-client.js +44 -11
  161. package/src/harness/session-index.js +80 -80
  162. package/src/harness/session-list-index.js +147 -2
  163. package/src/lib/background-interaction-journal.js +2 -2
  164. package/src/lib/command-lifecycle-report.js +516 -0
  165. package/src/lib/mcp-stdio-environment.js +48 -0
  166. package/src/lib/mcp-stdio-executable-identity.js +552 -17
  167. package/src/lib/mcp-stdio-package-materialization.js +1424 -0
  168. package/src/lib/packer/native-update-state.js +979 -1
  169. package/src/lib/packer/pack-update-applier.js +1050 -125
  170. package/src/lib/packer/pkg-config-generator.js +1 -0
  171. package/src/lib/paths.js +53 -0
  172. package/src/lib/process-execution-broker/index.js +180 -37
  173. package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
  174. package/src/lib/recent-session.js +17 -17
  175. package/src/lib/secure-file-identity.js +47 -0
  176. package/src/lib/secure-fs.js +21 -2
  177. package/src/lib/session-anti-rollback-anchor.js +766 -0
  178. package/src/lib/session-host-lease.js +492 -0
  179. package/src/lib/session-persistence-failure.js +109 -0
  180. package/src/lib/session-picker.js +26 -3
  181. package/src/lib/session-search.js +25 -11
  182. package/src/lib/side-effect-ledger-store.js +2 -2
  183. package/src/lib/skill-execution-authority.js +13 -9
  184. package/src/lib/turn-binding-store.js +2 -2
  185. package/src/repl/agent-repl.js +309 -39
  186. package/src/runtime/agent-core.js +88 -5
  187. package/src/runtime/cleanup-deadline.js +143 -0
  188. package/src/runtime/headless-runner.js +357 -139
  189. package/src/runtime/headless-stream.js +207 -28
  190. package/src/runtime/output-backpressure.js +53 -0
  191. package/src/assets/web-panel/assets/MarkdownRenderer-CU-JB2Ns.js +0 -1
  192. package/src/assets/web-panel/assets/Providers-DDCf_nIw.js +0 -1
  193. package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +0 -1
  194. package/src/assets/web-panel/assets/index-CzrCHrFy.js +0 -1
  195. package/src/assets/web-panel/assets/index-OJsGskZw.js +0 -1
@@ -23,6 +23,7 @@ import {
23
23
  sameOpenedFileIdentity,
24
24
  } from "./file-identity.js";
25
25
  import {
26
+ NATIVE_GENERATION_TRANSACTION_SCHEMA,
26
27
  NATIVE_UPDATE_LINEAGE_SCHEMA,
27
28
  NATIVE_UPDATE_RESULT_SCHEMA,
28
29
  SHA256_HEX,
@@ -101,6 +102,8 @@ export async function scheduleReplace(ctx = {}) {
101
102
  "last-known-good backup": statePaths.backupPath,
102
103
  "update lock": statePaths.lockPath,
103
104
  "update lineage": statePaths.lineagePath,
105
+ "generation transaction journal": statePaths.journalPath,
106
+ "retired generation transaction journal": statePaths.lastJournalPath,
104
107
  "update result": statePaths.resultPath,
105
108
  "last consumed update result": statePaths.lastResultPath,
106
109
  })) {
@@ -145,6 +148,12 @@ export async function scheduleReplace(ctx = {}) {
145
148
  "RESULT_PENDING",
146
149
  );
147
150
  }
151
+ if (lstatOrNull(statePaths.journalPath)) {
152
+ throw new ApplyError(
153
+ `an interrupted native generation requires restart recovery: ${statePaths.journalPath}`,
154
+ "RECOVERY_REQUIRED",
155
+ );
156
+ }
148
157
 
149
158
  const updateLock = acquireUpdateLock(statePaths.lockPath, platform);
150
159
  const transactionId = crypto.randomUUID();
@@ -160,6 +169,8 @@ export async function scheduleReplace(ctx = {}) {
160
169
  for (const [label, filePath] of Object.entries({
161
170
  "last-known-good backup": statePaths.backupPath,
162
171
  "update lineage": statePaths.lineagePath,
172
+ "generation transaction journal": statePaths.journalPath,
173
+ "retired generation transaction journal": statePaths.lastJournalPath,
163
174
  "update result": statePaths.resultPath,
164
175
  "last consumed update result": statePaths.lastResultPath,
165
176
  })) {
@@ -183,6 +194,10 @@ export async function scheduleReplace(ctx = {}) {
183
194
  const priorBackupSha = hadPriorBackup
184
195
  ? stableSha256(statePaths.backupPath, "existing last-known-good backup")
185
196
  : null;
197
+ const hadPriorLineage = Boolean(lstatOrNull(statePaths.lineagePath));
198
+ const priorLineageSha = hadPriorLineage
199
+ ? stableSha256(statePaths.lineagePath, "existing native update lineage")
200
+ : null;
186
201
 
187
202
  if (platform === "win32") {
188
203
  return scheduleWindowsTransaction({
@@ -198,7 +213,9 @@ export async function scheduleReplace(ctx = {}) {
198
213
  hadTarget,
199
214
  hadAlias,
200
215
  hadPriorBackup,
216
+ hadPriorLineage,
201
217
  priorBackupSha,
218
+ priorLineageSha,
202
219
  parentPid,
203
220
  restart: Boolean(restart),
204
221
  verify: Boolean(verify),
@@ -218,6 +235,9 @@ export async function scheduleReplace(ctx = {}) {
218
235
  targetBeforeSha,
219
236
  hadTarget,
220
237
  hadPriorBackup,
238
+ hadPriorLineage,
239
+ priorBackupSha,
240
+ priorLineageSha,
221
241
  restart: Boolean(restart),
222
242
  verify: Boolean(verify),
223
243
  spawnImpl,
@@ -245,6 +265,9 @@ function applyPosixUpdate(ctx) {
245
265
  targetBeforeSha,
246
266
  hadTarget,
247
267
  hadPriorBackup,
268
+ hadPriorLineage,
269
+ priorBackupSha,
270
+ priorLineageSha,
248
271
  restart,
249
272
  verify,
250
273
  spawnImpl,
@@ -253,7 +276,61 @@ function applyPosixUpdate(ctx) {
253
276
  } = ctx;
254
277
  const targetPath = layout.canonicalPath;
255
278
  let backupStagingPath = null;
256
- let backupCommitted = false;
279
+ const snapshotPaths = generationSnapshotPaths(targetPath, transactionId);
280
+ const journal = makeGenerationJournal({
281
+ transactionId,
282
+ operation: "update",
283
+ expectedSha,
284
+ hadTarget,
285
+ targetBeforeSha,
286
+ hadBackup: hadPriorBackup,
287
+ backupBeforeSha: priorBackupSha,
288
+ hadLineage: hadPriorLineage,
289
+ lineageBeforeSha: priorLineageSha,
290
+ });
291
+
292
+ try {
293
+ if (hadTarget) {
294
+ atomicCopyFile(targetPath, snapshotPaths.priorTargetPath, {
295
+ preserveModeFrom: targetPath,
296
+ updateLock,
297
+ });
298
+ assertExpectedFileHash(
299
+ snapshotPaths.priorTargetPath,
300
+ targetBeforeSha,
301
+ "target generation snapshot",
302
+ );
303
+ }
304
+ if (hadPriorBackup) {
305
+ atomicCopyFile(statePaths.backupPath, snapshotPaths.priorBackupPath, {
306
+ preserveModeFrom: statePaths.backupPath,
307
+ updateLock,
308
+ });
309
+ assertExpectedFileHash(
310
+ snapshotPaths.priorBackupPath,
311
+ priorBackupSha,
312
+ "backup generation snapshot",
313
+ );
314
+ }
315
+ if (hadPriorLineage) {
316
+ atomicCopyFile(statePaths.lineagePath, snapshotPaths.priorLineagePath, {
317
+ preserveModeFrom: statePaths.lineagePath,
318
+ updateLock,
319
+ });
320
+ assertExpectedFileHash(
321
+ snapshotPaths.priorLineagePath,
322
+ priorLineageSha,
323
+ "lineage generation snapshot",
324
+ );
325
+ }
326
+ } catch (error) {
327
+ cleanupGenerationSnapshots(snapshotPaths);
328
+ throw asApplyError(
329
+ error,
330
+ "GENERATION_SNAPSHOT_FAILED",
331
+ "could not preserve the native generation pre-state",
332
+ );
333
+ }
257
334
 
258
335
  if (hadTarget) {
259
336
  backupStagingPath = uniqueSibling(statePaths.backupPath, "pending");
@@ -269,10 +346,35 @@ function applyPosixUpdate(ctx) {
269
346
  );
270
347
  } catch (error) {
271
348
  removeRegularFileIfPresent(backupStagingPath);
349
+ cleanupGenerationSnapshots(snapshotPaths);
272
350
  throw asApplyError(error, "BACKUP_FAILED", "could not create backup");
273
351
  }
274
352
  }
275
353
 
354
+ try {
355
+ writeGenerationJournalPhase(
356
+ statePaths.journalPath,
357
+ journal,
358
+ "prepared",
359
+ "rollback",
360
+ updateLock,
361
+ );
362
+ } catch (error) {
363
+ removeRegularFileIfPresent(backupStagingPath);
364
+ if (lstatOrNull(statePaths.journalPath)) {
365
+ throw new ApplyError(
366
+ `native generation intent persistence is unknown: ${error.message}`,
367
+ "ROLLBACK_FAILED",
368
+ );
369
+ }
370
+ cleanupGenerationSnapshots(snapshotPaths);
371
+ throw asApplyError(
372
+ error,
373
+ "JOURNAL_WRITE_FAILED",
374
+ "could not persist native generation intent",
375
+ );
376
+ }
377
+
276
378
  try {
277
379
  fs.chmodSync(resolvedNewPath, 0o755);
278
380
  } catch {
@@ -294,8 +396,36 @@ function applyPosixUpdate(ctx) {
294
396
  assertUpdateLockOwned(updateLock);
295
397
  fs.renameSync(resolvedNewPath, targetPath);
296
398
  fsyncDirectory(path.dirname(targetPath));
399
+ writeGenerationJournalPhase(
400
+ statePaths.journalPath,
401
+ journal,
402
+ "target-committed",
403
+ "rollback",
404
+ updateLock,
405
+ );
297
406
  } catch (error) {
298
407
  removeRegularFileIfPresent(backupStagingPath);
408
+ try {
409
+ rollbackFailedPosixUpdate({
410
+ targetPath,
411
+ statePaths,
412
+ transactionId,
413
+ hadTarget,
414
+ targetBeforeSha,
415
+ hadPriorBackup,
416
+ hadPriorLineage,
417
+ priorBackupSha,
418
+ priorLineageSha,
419
+ snapshotPaths,
420
+ journal,
421
+ updateLock,
422
+ });
423
+ } catch (recoveryError) {
424
+ throw new ApplyError(
425
+ `target commit failed and its generation journal could not be retired: ${recoveryError.message}`,
426
+ "ROLLBACK_FAILED",
427
+ );
428
+ }
299
429
  if (error instanceof ApplyError) throw error;
300
430
  throw new ApplyError(`rename failed: ${error.message}`, "RENAME_FAILED");
301
431
  }
@@ -316,15 +446,37 @@ function applyPosixUpdate(ctx) {
316
446
  if (!commitFailure && verify) {
317
447
  commitFailure = verificationFailure(targetPath, verifyImpl);
318
448
  }
449
+ if (!commitFailure) {
450
+ try {
451
+ writeGenerationJournalPhase(
452
+ statePaths.journalPath,
453
+ journal,
454
+ "verified",
455
+ "rollback",
456
+ updateLock,
457
+ );
458
+ } catch (error) {
459
+ commitFailure = new ApplyError(
460
+ `could not persist native generation verification: ${error.message}`,
461
+ "JOURNAL_WRITE_FAILED",
462
+ );
463
+ }
464
+ }
319
465
  if (!commitFailure) {
320
466
  try {
321
467
  if (hadTarget) {
322
468
  assertUpdateLockOwned(updateLock);
323
469
  fs.renameSync(backupStagingPath, statePaths.backupPath);
324
470
  backupStagingPath = null;
325
- backupCommitted = true;
326
471
  fsyncDirectory(path.dirname(targetPath));
327
472
  }
473
+ writeGenerationJournalPhase(
474
+ statePaths.journalPath,
475
+ journal,
476
+ "backup-committed",
477
+ "rollback",
478
+ updateLock,
479
+ );
328
480
  } catch (error) {
329
481
  commitFailure = new ApplyError(
330
482
  `could not persist last-known-good backup: ${error.message}`,
@@ -344,6 +496,13 @@ function applyPosixUpdate(ctx) {
344
496
  }),
345
497
  updateLock,
346
498
  );
499
+ writeGenerationJournalPhase(
500
+ statePaths.journalPath,
501
+ journal,
502
+ "lineage-committed",
503
+ "rollback",
504
+ updateLock,
505
+ );
347
506
  } catch (error) {
348
507
  commitFailure = new ApplyError(
349
508
  `could not persist update lineage: ${error.message}`,
@@ -359,15 +518,42 @@ function applyPosixUpdate(ctx) {
359
518
  transactionId,
360
519
  hadTarget,
361
520
  targetBeforeSha,
362
- rollbackSourcePath: backupStagingPath || statePaths.backupPath,
363
- backupCommitted,
364
521
  hadPriorBackup,
522
+ hadPriorLineage,
523
+ priorBackupSha,
524
+ priorLineageSha,
525
+ snapshotPaths,
526
+ journal,
365
527
  updateLock,
366
528
  });
367
529
  removeRegularFileIfPresent(backupStagingPath);
368
530
  throw commitFailure;
369
531
  }
370
532
 
533
+ try {
534
+ writeGenerationJournalPhase(
535
+ statePaths.journalPath,
536
+ journal,
537
+ "committed",
538
+ "commit",
539
+ updateLock,
540
+ );
541
+ } catch (error) {
542
+ throw new ApplyError(
543
+ `native generation commit decision is unknown: ${error.message}`,
544
+ "ROLLBACK_FAILED",
545
+ );
546
+ }
547
+ try {
548
+ retireGenerationJournal(statePaths, journal, updateLock);
549
+ } catch (error) {
550
+ throw new ApplyError(
551
+ `native generation committed but journal retirement failed: ${error.message}`,
552
+ "ROLLBACK_FAILED",
553
+ );
554
+ }
555
+ cleanupGenerationSnapshots(snapshotPaths);
556
+
371
557
  restartVerifiedExecutable(targetPath, restart, spawnImpl);
372
558
  return buildPlan({
373
559
  platform: "posix",
@@ -390,9 +576,12 @@ function rollbackFailedPosixUpdate(ctx) {
390
576
  transactionId,
391
577
  hadTarget,
392
578
  targetBeforeSha,
393
- rollbackSourcePath,
394
- backupCommitted,
395
579
  hadPriorBackup,
580
+ hadPriorLineage,
581
+ priorBackupSha,
582
+ priorLineageSha,
583
+ snapshotPaths,
584
+ journal,
396
585
  updateLock,
397
586
  } = ctx;
398
587
  const failedPath = uniqueSibling(targetPath, "failed");
@@ -406,8 +595,8 @@ function rollbackFailedPosixUpdate(ctx) {
406
595
  } catch {
407
596
  /* preserving rejected bytes must never block restoration */
408
597
  }
409
- atomicCopyFile(rollbackSourcePath, targetPath, {
410
- preserveModeFrom: rollbackSourcePath,
598
+ atomicCopyFile(snapshotPaths.priorTargetPath, targetPath, {
599
+ preserveModeFrom: snapshotPaths.priorTargetPath,
411
600
  updateLock,
412
601
  });
413
602
  assertExpectedFileHash(
@@ -415,43 +604,68 @@ function rollbackFailedPosixUpdate(ctx) {
415
604
  targetBeforeSha,
416
605
  "restored executable",
417
606
  );
418
- // When the pending backup was never committed, the old target,
419
- // `.previous`, and lineage are once again the exact pre-transaction
420
- // state. Preserve that lineage instead of fabricating a relationship to
421
- // a backup that was never installed.
422
- if (backupCommitted) {
423
- atomicWriteJson(
424
- statePaths.lineagePath,
425
- makeLineage({
426
- transactionId,
427
- operation: "rolled-back",
428
- currentSha256: targetBeforeSha,
429
- previousSha256: targetBeforeSha,
430
- }),
431
- updateLock,
607
+ } else {
608
+ const currentTarget = lstatOrNull(targetPath);
609
+ if (currentTarget) {
610
+ assertExpectedFileHash(
611
+ targetPath,
612
+ journal.expectedSha256,
613
+ "failed fresh target",
432
614
  );
433
- } else if (!hadPriorBackup) {
434
615
  assertUpdateLockOwned(updateLock);
435
- fs.renameSync(rollbackSourcePath, statePaths.backupPath);
616
+ fs.renameSync(targetPath, failedPath);
436
617
  fsyncDirectory(path.dirname(targetPath));
437
- atomicWriteJson(
438
- statePaths.lineagePath,
439
- makeLineage({
440
- transactionId,
441
- operation: "rolled-back",
442
- currentSha256: targetBeforeSha,
443
- previousSha256: targetBeforeSha,
444
- }),
445
- updateLock,
446
- );
447
618
  }
448
- } else {
619
+ }
620
+
621
+ if (hadPriorBackup) {
622
+ atomicCopyFile(snapshotPaths.priorBackupPath, statePaths.backupPath, {
623
+ preserveModeFrom: snapshotPaths.priorBackupPath,
624
+ updateLock,
625
+ });
626
+ assertExpectedFileHash(
627
+ statePaths.backupPath,
628
+ priorBackupSha,
629
+ "restored last-known-good backup",
630
+ );
631
+ } else if (lstatOrNull(statePaths.backupPath)) {
632
+ if (!hadTarget) {
633
+ throw new Error("fresh update created an unexpected backup");
634
+ }
635
+ assertExpectedFileHash(
636
+ statePaths.backupPath,
637
+ targetBeforeSha,
638
+ "transaction-created last-known-good backup",
639
+ );
449
640
  assertUpdateLockOwned(updateLock);
450
- fs.renameSync(targetPath, failedPath);
641
+ removeRegularFileIfPresent(statePaths.backupPath);
451
642
  fsyncDirectory(path.dirname(targetPath));
643
+ }
644
+
645
+ if (hadPriorLineage) {
646
+ atomicCopyFile(snapshotPaths.priorLineagePath, statePaths.lineagePath, {
647
+ preserveModeFrom: snapshotPaths.priorLineagePath,
648
+ updateLock,
649
+ });
650
+ assertExpectedFileHash(
651
+ statePaths.lineagePath,
652
+ priorLineageSha,
653
+ "restored native update lineage",
654
+ );
655
+ } else if (lstatOrNull(statePaths.lineagePath)) {
656
+ const lineage = readNativeLineage(statePaths.lineagePath);
657
+ if (
658
+ lineage.transactionId !== transactionId ||
659
+ lineage.currentSha256 !== journal.expectedSha256
660
+ ) {
661
+ throw new Error("transaction-created lineage changed before rollback");
662
+ }
452
663
  assertUpdateLockOwned(updateLock);
453
664
  removeRegularFileIfPresent(statePaths.lineagePath);
665
+ fsyncDirectory(path.dirname(targetPath));
454
666
  }
667
+ retireGenerationJournal(statePaths, journal, updateLock);
668
+ cleanupGenerationSnapshots(snapshotPaths);
455
669
  } catch (error) {
456
670
  throw new ApplyError(
457
671
  `new binary failed and rollback failed: ${error.message}`,
@@ -474,7 +688,9 @@ function scheduleWindowsTransaction(ctx) {
474
688
  hadTarget,
475
689
  hadAlias,
476
690
  hadPriorBackup = false,
691
+ hadPriorLineage = false,
477
692
  priorBackupSha = null,
693
+ priorLineageSha = null,
478
694
  parentPid,
479
695
  restart,
480
696
  verify,
@@ -492,6 +708,7 @@ function scheduleWindowsTransaction(ctx) {
492
708
  aliasPath: layout.aliasPath,
493
709
  backupPath: statePaths.backupPath,
494
710
  lineagePath: statePaths.lineagePath,
711
+ journalPath: statePaths.journalPath,
495
712
  lockPath: statePaths.lockPath,
496
713
  resultPath: statePaths.resultPath,
497
714
  lockToken: updateLock.token,
@@ -502,7 +719,9 @@ function scheduleWindowsTransaction(ctx) {
502
719
  hadTarget,
503
720
  hadAlias,
504
721
  hadPriorBackup,
722
+ hadPriorLineage,
505
723
  priorBackupSha256: priorBackupSha,
724
+ priorLineageSha256: priorLineageSha,
506
725
  parentPid,
507
726
  restart,
508
727
  verify,
@@ -551,11 +770,6 @@ function scheduleWindowsTransaction(ctx) {
551
770
  // post-transfer paths always consume the ChildProcess error event.
552
771
  child.once("error", (error) => {
553
772
  childError = error;
554
- if (ready) {
555
- removeOwnedLockFile(updateLock);
556
- removeRegularFileIfPresent(sidecarPath);
557
- removeRegularFileIfPresent(readyPath);
558
- }
559
773
  });
560
774
  }
561
775
  try {
@@ -606,6 +820,7 @@ function scheduleWindowsTransaction(ctx) {
606
820
  });
607
821
  } catch (error) {
608
822
  if (sidecarPath) removeRegularFileIfPresent(sidecarPath);
823
+ if (sidecarPath) removeRegularFileIfPresent(`${sidecarPath}.journal.ps1`);
609
824
  if (sidecarPath) removeRegularFileIfPresent(`${sidecarPath}.ready`);
610
825
  if (error instanceof ApplyError) throw error;
611
826
  throw new ApplyError(
@@ -615,6 +830,167 @@ function scheduleWindowsTransaction(ctx) {
615
830
  }
616
831
  }
617
832
 
833
+ function windowsGenerationJournalHelperSource() {
834
+ return String.raw`param()
835
+ $ErrorActionPreference = 'Stop'
836
+ $Action = [string]$env:JOURNAL_ACTION
837
+ $JournalPath = [string]$env:JOURNAL_FILE
838
+ $RetiredPath = [string]$env:JOURNAL_RETIRED
839
+ $TransactionId = [string]$env:TRANSACTION_ID
840
+ $Phase = [string]$env:JOURNAL_PHASE
841
+ $Decision = [string]$env:JOURNAL_DECISION
842
+ $HashPattern = '^[a-f0-9]{64}$'
843
+ $AllowedPhases = @('prepared','target-committed','alias-committed','verified','backup-committed','lineage-committed','committed')
844
+ $AllowedActions = @('write','retire')
845
+ $ParsedId = [guid]::Empty
846
+ if ($AllowedActions -notcontains $Action) { throw 'Invalid journal action' }
847
+ if (-not [guid]::TryParse($TransactionId, [ref]$ParsedId)) { throw 'Invalid transaction identifier' }
848
+ if ($AllowedPhases -notcontains $Phase) { throw 'Invalid transaction phase' }
849
+ if (@('rollback','commit') -notcontains $Decision) { throw 'Invalid transaction decision' }
850
+ if (($Phase -eq 'committed') -ne ($Decision -eq 'commit')) { throw 'Invalid phase decision' }
851
+
852
+ if ($Action -eq 'write' -and $Phase -eq 'prepared') {
853
+ if ([IO.File]::Exists($JournalPath)) { throw 'A generation journal already exists' }
854
+ $Bytes = [Convert]::FromBase64String([string]$env:JOURNAL_INITIAL_BASE64)
855
+ $Journal = [Text.Encoding]::UTF8.GetString($Bytes) | ConvertFrom-Json
856
+ $ParentSource = @'
857
+ using System;
858
+ using System.Runtime.InteropServices;
859
+ public static class CcParentProcess {
860
+ [StructLayout(LayoutKind.Sequential)]
861
+ private struct PBI {
862
+ public IntPtr Reserved1;
863
+ public IntPtr PebBaseAddress;
864
+ public IntPtr Reserved2_0;
865
+ public IntPtr Reserved2_1;
866
+ public IntPtr UniqueProcessId;
867
+ public IntPtr InheritedFromUniqueProcessId;
868
+ }
869
+ [DllImport("ntdll.dll")]
870
+ private static extern int NtQueryInformationProcess(IntPtr process, int infoClass, ref PBI info, int size, out int returned);
871
+ [DllImport("kernel32.dll")]
872
+ private static extern IntPtr GetCurrentProcess();
873
+ public static int Get() {
874
+ PBI info = new PBI();
875
+ int returned;
876
+ int status = NtQueryInformationProcess(GetCurrentProcess(), 0, ref info, Marshal.SizeOf(info), out returned);
877
+ if (status != 0) { throw new InvalidOperationException("Parent process query failed"); }
878
+ return info.InheritedFromUniqueProcessId.ToInt32();
879
+ }
880
+ }
881
+ '@
882
+ Add-Type -TypeDefinition $ParentSource -Language CSharp
883
+ $Journal | Add-Member -NotePropertyName ownerPid -NotePropertyValue ([CcParentProcess]::Get())
884
+ } else {
885
+ if (-not [IO.File]::Exists($JournalPath)) { throw 'Generation journal is missing' }
886
+ $Journal = Get-Content -Raw -LiteralPath $JournalPath | ConvertFrom-Json
887
+ }
888
+
889
+ $ValidBefore = {
890
+ param($Present, $Digest)
891
+ return (($Present -is [bool]) -and ((-not $Present -and $null -eq $Digest) -or ($Present -and [string]$Digest -match $HashPattern)))
892
+ }
893
+ $Valid = (
894
+ [string]$Journal.schema -eq 'chainlesschain.native-install-transaction.v1' -and
895
+ [string]$Journal.transactionId -eq $TransactionId -and
896
+ @('update','rescue') -contains [string]$Journal.operation -and
897
+ ($Journal.ownerPid -is [int] -or $Journal.ownerPid -is [long]) -and
898
+ [int64]$Journal.ownerPid -gt 0 -and
899
+ $AllowedPhases -contains [string]$Journal.phase -and
900
+ @('rollback','commit') -contains [string]$Journal.decision -and
901
+ [string]$Journal.expectedSha256 -match $HashPattern -and
902
+ (& $ValidBefore $Journal.hadTarget $Journal.targetBeforeSha256) -and
903
+ (& $ValidBefore $Journal.hadAlias $Journal.aliasBeforeSha256) -and
904
+ (& $ValidBefore $Journal.hadBackup $Journal.backupBeforeSha256) -and
905
+ (& $ValidBefore $Journal.hadLineage $Journal.lineageBeforeSha256)
906
+ )
907
+ if (-not $Valid) { throw 'Generation journal failed schema validation' }
908
+ if ([string]$Journal.transactionId -ne $TransactionId) { throw 'Generation journal ownership changed' }
909
+
910
+ if ($Action -eq 'retire') {
911
+ if ([string]$Journal.phase -ne $Phase -or [string]$Journal.decision -ne $Decision) {
912
+ throw 'Generation journal decision changed before retirement'
913
+ }
914
+ $ReplacedPath = Join-Path (Split-Path -Parent $JournalPath) ('.chainlesschain.journal-retired-previous-' + [guid]::NewGuid().ToString('N'))
915
+ try {
916
+ if ([IO.File]::Exists($RetiredPath)) {
917
+ [IO.File]::Replace($JournalPath, $RetiredPath, $ReplacedPath, $true)
918
+ } else {
919
+ [IO.File]::Move($JournalPath, $RetiredPath)
920
+ }
921
+ $Final = [IO.File]::Open($RetiredPath, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
922
+ try { $Final.Flush($true) } finally { $Final.Dispose() }
923
+ } finally {
924
+ if ([IO.File]::Exists($ReplacedPath)) {
925
+ try { [IO.File]::Delete($ReplacedPath) } catch { }
926
+ }
927
+ }
928
+ exit 0
929
+ }
930
+
931
+ if ($Phase -eq 'prepared') {
932
+ $Snapshots = [Collections.Generic.Dictionary[string,string]]::new([StringComparer]::OrdinalIgnoreCase)
933
+ if ($Journal.hadTarget) { $Snapshots.Add([string]$env:TARGET_SNAPSHOT, [string]$Journal.targetBeforeSha256) }
934
+ if ($Journal.hadAlias) { $Snapshots.Add([string]$env:ALIAS_BACKUP, [string]$Journal.aliasBeforeSha256) }
935
+ if ($Journal.hadBackup) { $Snapshots.Add([string]$env:BACKUP_SNAPSHOT, [string]$Journal.backupBeforeSha256) }
936
+ if ($Journal.hadLineage) { $Snapshots.Add([string]$env:LINEAGE_SNAPSHOT, [string]$Journal.lineageBeforeSha256) }
937
+ foreach ($Snapshot in $Snapshots.GetEnumerator()) {
938
+ if (-not [IO.File]::Exists($Snapshot.Key)) { throw 'A recovery snapshot is missing' }
939
+ $SnapshotStream = [IO.File]::Open($Snapshot.Key, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
940
+ try {
941
+ $Hasher = [Security.Cryptography.SHA256]::Create()
942
+ try {
943
+ $Observed = ([BitConverter]::ToString($Hasher.ComputeHash($SnapshotStream))).Replace('-', '').ToLowerInvariant()
944
+ } finally { $Hasher.Dispose() }
945
+ if ($Observed -ne $Snapshot.Value) { throw 'A recovery snapshot changed while staging' }
946
+ $SnapshotStream.Flush($true)
947
+ } finally { $SnapshotStream.Dispose() }
948
+ }
949
+ }
950
+
951
+ if ($Phase -ne 'prepared') {
952
+ $Transitions = @{
953
+ 'prepared' = @('target-committed')
954
+ 'target-committed' = @('alias-committed','verified')
955
+ 'alias-committed' = @('verified')
956
+ 'verified' = @('backup-committed','lineage-committed')
957
+ 'backup-committed' = @('lineage-committed')
958
+ 'lineage-committed' = @('committed')
959
+ }
960
+ if ($Transitions[[string]$Journal.phase] -notcontains $Phase) {
961
+ throw 'Generation journal phase transition is invalid'
962
+ }
963
+ }
964
+
965
+ $Journal.phase = $Phase
966
+ $Journal.decision = $Decision
967
+ $Journal.updatedAt = [DateTimeOffset]::UtcNow.ToString('o')
968
+ $Directory = Split-Path -Parent $JournalPath
969
+ $StagingPath = Join-Path $Directory ('.chainlesschain.journal-' + [guid]::NewGuid().ToString('N'))
970
+ $ReplacedPath = Join-Path $Directory ('.chainlesschain.journal-previous-' + [guid]::NewGuid().ToString('N'))
971
+ try {
972
+ $Payload = $Journal | ConvertTo-Json -Compress -Depth 8
973
+ $PayloadBytes = [Text.UTF8Encoding]::new($false).GetBytes($Payload + [Environment]::NewLine)
974
+ $Stream = [IO.File]::Open($StagingPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
975
+ try {
976
+ $Stream.Write($PayloadBytes, 0, $PayloadBytes.Length)
977
+ $Stream.Flush($true)
978
+ } finally { $Stream.Dispose() }
979
+ if ([IO.File]::Exists($JournalPath)) {
980
+ [IO.File]::Replace($StagingPath, $JournalPath, $ReplacedPath, $true)
981
+ [IO.File]::Delete($ReplacedPath)
982
+ } else {
983
+ [IO.File]::Move($StagingPath, $JournalPath)
984
+ }
985
+ $Final = [IO.File]::Open($JournalPath, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
986
+ try { $Final.Flush($true) } finally { $Final.Dispose() }
987
+ } finally {
988
+ if ([IO.File]::Exists($StagingPath)) { [IO.File]::Delete($StagingPath) }
989
+ if ([IO.File]::Exists($ReplacedPath)) { [IO.File]::Delete($ReplacedPath) }
990
+ }
991
+ `;
992
+ }
993
+
618
994
  /**
619
995
  * Windows sidecar for both forward update and independent rescue.
620
996
  * Every caller must pass the observed pre-state and lock token.
@@ -627,6 +1003,7 @@ export function writeWindowsSidecar(ctx) {
627
1003
  aliasPath = null,
628
1004
  backupPath = `${targetExePath}.previous`,
629
1005
  lineagePath = `${targetExePath}.update-lineage.json`,
1006
+ journalPath = `${targetExePath}.update-transaction.json`,
630
1007
  lockPath = `${targetExePath}.update.lock`,
631
1008
  resultPath = `${targetExePath}.update-result.json`,
632
1009
  lockToken,
@@ -637,7 +1014,9 @@ export function writeWindowsSidecar(ctx) {
637
1014
  hadTarget,
638
1015
  hadAlias = false,
639
1016
  hadPriorBackup = false,
1017
+ hadPriorLineage = false,
640
1018
  priorBackupSha256 = null,
1019
+ priorLineageSha256 = null,
641
1020
  parentPid,
642
1021
  restart,
643
1022
  verify = true,
@@ -665,19 +1044,37 @@ export function writeWindowsSidecar(ctx) {
665
1044
  "MISSING_PRESTATE",
666
1045
  );
667
1046
  }
1047
+ if (typeof hadPriorLineage !== "boolean") {
1048
+ throw new ApplyError(
1049
+ "sidecar requires explicit prior lineage state",
1050
+ "MISSING_PRESTATE",
1051
+ );
1052
+ }
668
1053
  const expectedSha = validateExpectedSha(expectedSha256);
669
1054
  if (hadTarget) validateExpectedSha(targetBeforeSha256, "targetBeforeSha256");
670
1055
  if (hadAlias) validateExpectedSha(aliasBeforeSha256, "aliasBeforeSha256");
671
1056
  if (hadPriorBackup) {
672
1057
  validateExpectedSha(priorBackupSha256, "priorBackupSha256");
673
1058
  }
674
- if (!lockToken || !/^[0-9]+:[0-9a-f-]{32,36}$/i.test(lockToken)) {
1059
+ if (hadPriorLineage) {
1060
+ validateExpectedSha(priorLineageSha256, "priorLineageSha256");
1061
+ }
1062
+ if (
1063
+ !lockToken ||
1064
+ !/^[1-9][0-9]*:(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.test(
1065
+ lockToken,
1066
+ )
1067
+ ) {
675
1068
  throw new ApplyError(
676
1069
  "a valid lock ownership token is required",
677
1070
  "BAD_LOCK_TOKEN",
678
1071
  );
679
1072
  }
680
- if (!/^[0-9a-f-]{36}$/i.test(transactionId)) {
1073
+ if (
1074
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
1075
+ transactionId,
1076
+ )
1077
+ ) {
681
1078
  throw new ApplyError(
682
1079
  "a valid transactionId is required",
683
1080
  "BAD_TRANSACTION_ID",
@@ -721,16 +1118,31 @@ export function writeWindowsSidecar(ctx) {
721
1118
  }
722
1119
  const readyPath = `${sidecarPath}.ready`;
723
1120
  const readyTempPath = `${readyPath}.tmp-${suffix}`;
1121
+ const journalHelperPath = `${sidecarPath}.journal.ps1`;
724
1122
  const parentProbePath = `${sidecarPath}.parent`;
1123
+ const targetSnapshotPath = path.join(
1124
+ path.dirname(targetExePath),
1125
+ `.chainlesschain.target-prior-${transactionId}.exe`,
1126
+ );
1127
+ const backupSnapshotPath = path.join(
1128
+ path.dirname(targetExePath),
1129
+ `.chainlesschain.backup-prior-${transactionId}.exe`,
1130
+ );
1131
+ const lineageSnapshotPath = path.join(
1132
+ path.dirname(targetExePath),
1133
+ `.chainlesschain.lineage-prior-${transactionId}.json`,
1134
+ );
1135
+ const journalRetiredPath = `${journalPath}.last`;
725
1136
  const backupTempPath = `${backupPath}.tmp-${suffix}`;
726
- const currentTempPath = `${targetExePath}.rescue-current-${suffix}`;
1137
+ const currentTempPath = targetSnapshotPath;
727
1138
  const rollbackTempPath = `${targetExePath}.rollback-${suffix}`;
1139
+ const backupRollbackPath = `${backupPath}.rollback-${suffix}`;
728
1140
  const failedPath = `${targetExePath}.failed-${suffix}`;
729
1141
  const aliasCandidatePath = aliasPath
730
1142
  ? `${aliasPath}.candidate-${suffix}`
731
1143
  : targetExePath;
732
1144
  const aliasBackupPath = aliasPath
733
- ? `${aliasPath}.previous-${suffix}`
1145
+ ? path.join(path.dirname(aliasPath), `.cc.previous-${transactionId}.exe`)
734
1146
  : targetExePath;
735
1147
  const aliasRollbackPath = aliasPath
736
1148
  ? `${aliasPath}.rollback-${suffix}`
@@ -746,6 +1158,7 @@ export function writeWindowsSidecar(ctx) {
746
1158
  // spelling before READY_FILE is published and lock ownership transfers.
747
1159
  rawTempRoot,
748
1160
  sidecarPath,
1161
+ journalHelperPath,
749
1162
  readyPath,
750
1163
  readyTempPath,
751
1164
  parentProbePath,
@@ -753,11 +1166,17 @@ export function writeWindowsSidecar(ctx) {
753
1166
  targetExePath,
754
1167
  backupPath,
755
1168
  lineagePath,
1169
+ journalPath,
1170
+ journalRetiredPath,
756
1171
  lockPath,
757
1172
  resultPath,
758
1173
  backupTempPath,
759
1174
  currentTempPath,
1175
+ targetSnapshotPath,
1176
+ backupSnapshotPath,
1177
+ lineageSnapshotPath,
760
1178
  rollbackTempPath,
1179
+ backupRollbackPath,
761
1180
  failedPath,
762
1181
  lineageTempPath,
763
1182
  resultTempPath,
@@ -785,16 +1204,32 @@ export function writeWindowsSidecar(ctx) {
785
1204
  : null
786
1205
  : expectedSha,
787
1206
  });
788
- const rolledBackLineage = hadTarget
789
- ? makeLineage({
790
- transactionId,
791
- operation: "rolled-back",
792
- currentSha256: targetBeforeSha256,
793
- previousSha256: targetBeforeSha256,
794
- })
795
- : null;
796
- const rollbackSourcePath =
797
- operation === "update" ? backupTempPath : currentTempPath;
1207
+ const rollbackSourcePath = targetSnapshotPath;
1208
+ const initialJournal = {
1209
+ schema: NATIVE_GENERATION_TRANSACTION_SCHEMA,
1210
+ transactionId,
1211
+ operation,
1212
+ phase: "prepared",
1213
+ decision: "rollback",
1214
+ expectedSha256: expectedSha,
1215
+ hadTarget,
1216
+ targetBeforeSha256: hadTarget ? targetBeforeSha256 : null,
1217
+ hadAlias,
1218
+ aliasBeforeSha256: hadAlias ? aliasBeforeSha256 : null,
1219
+ hadBackup: hadPriorBackup,
1220
+ backupBeforeSha256: hadPriorBackup ? priorBackupSha256 : null,
1221
+ hadLineage: hadPriorLineage,
1222
+ lineageBeforeSha256: hadPriorLineage ? priorLineageSha256 : null,
1223
+ updatedAt: null,
1224
+ };
1225
+ const initialJournalBase64 = Buffer.from(
1226
+ JSON.stringify(initialJournal),
1227
+ "utf8",
1228
+ ).toString("base64");
1229
+ const successLineageSha = crypto
1230
+ .createHash("sha256")
1231
+ .update(`${JSON.stringify(successLineage)}\r\n`, "utf8")
1232
+ .digest("hex");
798
1233
  const reparseChecks = buildWindowsReparseChecks(Object.values(allPaths));
799
1234
  const startupCheck = buildWindowsTimedStartupCheckLines();
800
1235
 
@@ -814,12 +1249,23 @@ export function writeWindowsSidecar(ctx) {
814
1249
  `set "CURRENT_TEMP=${currentTempPath}"`,
815
1250
  `set "ROLLBACK_SOURCE=${rollbackSourcePath}"`,
816
1251
  `set "ROLLBACK_TEMP=${rollbackTempPath}"`,
1252
+ `set "BACKUP_ROLLBACK=${backupRollbackPath}"`,
817
1253
  `set "FAILED_EXE=${failedPath}"`,
818
1254
  `set "ALIAS_CANDIDATE=${aliasCandidatePath}"`,
819
1255
  `set "ALIAS_BACKUP=${aliasBackupPath}"`,
820
1256
  `set "ALIAS_ROLLBACK=${aliasRollbackPath}"`,
821
1257
  `set "LINEAGE_FILE=${lineagePath}"`,
822
1258
  `set "LINEAGE_TEMP=${lineageTempPath}"`,
1259
+ `set "LINEAGE_SNAPSHOT=${lineageSnapshotPath}"`,
1260
+ `set "TARGET_SNAPSHOT=${targetSnapshotPath}"`,
1261
+ `set "BACKUP_SNAPSHOT=${backupSnapshotPath}"`,
1262
+ `set "JOURNAL_FILE=${journalPath}"`,
1263
+ `set "JOURNAL_RETIRED=${journalRetiredPath}"`,
1264
+ `set "JOURNAL_HELPER=${journalHelperPath}"`,
1265
+ `set "JOURNAL_INITIAL_BASE64=${initialJournalBase64}"`,
1266
+ 'set "JOURNAL_ACTION=write"',
1267
+ 'set "JOURNAL_PHASE=prepared"',
1268
+ 'set "JOURNAL_DECISION=rollback"',
823
1269
  `set "LOCK_FILE=${lockPath}"`,
824
1270
  `set "LOCK_TOKEN=${lockToken}"`,
825
1271
  `set "LOCK_TOKEN_SHA=${lockTokenSha}"`,
@@ -835,6 +1281,9 @@ export function writeWindowsSidecar(ctx) {
835
1281
  `set "HAD_ALIAS=${hadAlias ? "1" : "0"}"`,
836
1282
  `set "HAD_BACKUP=${hadPriorBackup ? "1" : "0"}"`,
837
1283
  `set "BACKUP_BEFORE_SHA=${priorBackupSha256 || ""}"`,
1284
+ `set "HAD_LINEAGE=${hadPriorLineage ? "1" : "0"}"`,
1285
+ `set "LINEAGE_BEFORE_SHA=${priorLineageSha256 || ""}"`,
1286
+ `set "LINEAGE_TRANSACTION_SHA=${successLineageSha}"`,
838
1287
  `set "MANAGE_ALIAS=${aliasPath ? "1" : "0"}"`,
839
1288
  'set "TARGET_COMMITTED=0"',
840
1289
  'set "ALIAS_COMMITTED=0"',
@@ -889,27 +1338,53 @@ export function writeWindowsSidecar(ctx) {
889
1338
  "if errorlevel 1 goto hashfailed",
890
1339
  'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto hashmismatch',
891
1340
  'if "%OPERATION%"=="rescue" goto stagerescue',
892
- 'if "%HAD_TARGET%"=="0" goto stagealias',
893
- 'copy /B /Y "%TARGET_EXE%" "%BACKUP_TEMP%" >NUL',
1341
+ 'if "%HAD_TARGET%"=="0" goto stagepriorstate',
1342
+ 'copy /B /Y "%TARGET_EXE%" "%TARGET_SNAPSHOT%" >NUL',
1343
+ "if errorlevel 1 goto backupfailed",
1344
+ 'set "HASH_PATH=%TARGET_SNAPSHOT%"',
1345
+ "call :hashpath",
1346
+ "if errorlevel 1 goto backupfailed",
1347
+ 'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto backupfailed',
1348
+ 'copy /B /Y "%TARGET_SNAPSHOT%" "%BACKUP_TEMP%" >NUL',
894
1349
  "if errorlevel 1 goto backupfailed",
895
1350
  'set "HASH_PATH=%BACKUP_TEMP%"',
896
1351
  "call :hashpath",
897
1352
  "if errorlevel 1 goto backupfailed",
898
1353
  'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto backupfailed',
899
- "goto stagealias",
1354
+ "goto stagepriorstate",
900
1355
  ":stagerescue",
901
- 'if "%HAD_TARGET%"=="0" goto stagealias',
1356
+ 'if "%HAD_TARGET%"=="0" goto stagepriorstate',
902
1357
  'copy /B /Y "%TARGET_EXE%" "%CURRENT_TEMP%" >NUL',
903
1358
  "if errorlevel 1 goto backupfailed",
904
1359
  'set "HASH_PATH=%CURRENT_TEMP%"',
905
1360
  "call :hashpath",
906
1361
  "if errorlevel 1 goto backupfailed",
907
1362
  'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto backupfailed',
1363
+ ":stagepriorstate",
1364
+ 'if "%HAD_BACKUP%"=="0" goto stagepriorlineage',
1365
+ 'copy /B /Y "%BACKUP_EXE%" "%BACKUP_SNAPSHOT%" >NUL',
1366
+ "if errorlevel 1 goto backupfailed",
1367
+ 'set "HASH_PATH=%BACKUP_SNAPSHOT%"',
1368
+ "call :hashpath",
1369
+ "if errorlevel 1 goto backupfailed",
1370
+ 'if /I not "%OBSERVED_SHA%"=="%BACKUP_BEFORE_SHA%" goto backupfailed',
1371
+ ":stagepriorlineage",
1372
+ 'if "%HAD_LINEAGE%"=="0" goto stagealias',
1373
+ 'copy /B /Y "%LINEAGE_FILE%" "%LINEAGE_SNAPSHOT%" >NUL',
1374
+ "if errorlevel 1 goto backupfailed",
1375
+ 'set "HASH_PATH=%LINEAGE_SNAPSHOT%"',
1376
+ "call :hashpath",
1377
+ "if errorlevel 1 goto backupfailed",
1378
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_BEFORE_SHA%" goto backupfailed',
908
1379
  ":stagealias",
909
- 'if "%MANAGE_ALIAS%"=="0" goto precommitcheck',
1380
+ 'if "%MANAGE_ALIAS%"=="0" goto persistprepared',
910
1381
  'if "%HAD_ALIAS%"=="0" goto makealiascandidate',
911
1382
  'copy /B /Y "%ALIAS_EXE%" "%ALIAS_BACKUP%" >NUL',
912
1383
  "if errorlevel 1 goto aliasstagefailed",
1384
+ 'set "HASH_PATH=%ALIAS_BACKUP%"',
1385
+ "call :hashpath",
1386
+ "if errorlevel 1 goto aliasstagefailed",
1387
+ 'if /I not "%OBSERVED_SHA%"=="%ALIAS_BEFORE_SHA%" goto aliasstagefailed',
913
1388
  ":makealiascandidate",
914
1389
  'copy /B /Y "%NEW_EXE%" "%ALIAS_CANDIDATE%" >NUL',
915
1390
  "if errorlevel 1 goto aliasstagefailed",
@@ -917,6 +1392,12 @@ export function writeWindowsSidecar(ctx) {
917
1392
  "call :hashpath",
918
1393
  "if errorlevel 1 goto aliasstagefailed",
919
1394
  'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto aliasstagefailed',
1395
+ ":persistprepared",
1396
+ 'set "JOURNAL_PHASE=prepared"',
1397
+ 'set "JOURNAL_DECISION=rollback"',
1398
+ "call :writejournal",
1399
+ "if errorlevel 1 goto journalpreparefailed",
1400
+ 'set "JOURNAL_INITIAL_BASE64="',
920
1401
  ":precommitcheck",
921
1402
  // Re-hash after all potentially slow backup work, immediately before move.
922
1403
  'set "HASH_PATH=%NEW_EXE%"',
@@ -933,10 +1414,16 @@ export function writeWindowsSidecar(ctx) {
933
1414
  'move /Y "%NEW_EXE%" "%TARGET_EXE%" >NUL',
934
1415
  "if errorlevel 1 goto movefailed",
935
1416
  'set "TARGET_COMMITTED=1"',
1417
+ 'set "JOURNAL_PHASE=target-committed"',
1418
+ "call :writejournal",
1419
+ "if errorlevel 1 goto journalphasefailed",
936
1420
  'if "%MANAGE_ALIAS%"=="0" goto verifytransaction',
937
1421
  'move /Y "%ALIAS_CANDIDATE%" "%ALIAS_EXE%" >NUL',
938
1422
  "if errorlevel 1 goto aliascommitfailed",
939
1423
  'set "ALIAS_COMMITTED=1"',
1424
+ 'set "JOURNAL_PHASE=alias-committed"',
1425
+ "call :writejournal",
1426
+ "if errorlevel 1 goto journalphasefailed",
940
1427
  ":verifytransaction",
941
1428
  // Hash committed pathnames before executing either binary or persisting
942
1429
  // backup/lineage. This catches a candidate pathname exchange in the final
@@ -955,22 +1442,46 @@ export function writeWindowsSidecar(ctx) {
955
1442
  ? [
956
1443
  'set "VERIFY_PATH=%TARGET_EXE%"',
957
1444
  ...startupCheck,
958
- 'if "%MANAGE_ALIAS%"=="0" goto writelineage',
1445
+ 'if "%MANAGE_ALIAS%"=="0" goto persistverified',
959
1446
  'set "VERIFY_PATH=%ALIAS_EXE%"',
960
1447
  ...startupCheck,
961
1448
  ]
962
1449
  : ["REM post-replace verification disabled"]),
1450
+ ":persistverified",
1451
+ 'set "JOURNAL_PHASE=verified"',
1452
+ "call :writejournal",
1453
+ "if errorlevel 1 goto journalphasefailed",
963
1454
  ":writelineage",
964
1455
  'if not "%OPERATION%"=="update" goto persistlineage',
965
1456
  'if "%HAD_TARGET%"=="0" goto persistlineage',
966
1457
  'move /Y "%BACKUP_TEMP%" "%BACKUP_EXE%" >NUL',
967
1458
  "if errorlevel 1 goto backupcommitfailed",
968
1459
  'set "BACKUP_COMMITTED=1"',
969
- 'set "ROLLBACK_SOURCE=%BACKUP_EXE%"',
1460
+ 'set "HASH_PATH=%BACKUP_EXE%"',
1461
+ "call :hashpath",
1462
+ "if errorlevel 1 goto backupcommitfailed",
1463
+ 'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto backupcommitfailed',
1464
+ 'set "JOURNAL_PHASE=backup-committed"',
1465
+ "call :writejournal",
1466
+ "if errorlevel 1 goto journalphasefailed",
970
1467
  ":persistlineage",
971
1468
  `> "%LINEAGE_TEMP%" echo ${JSON.stringify(successLineage)}`,
972
1469
  'move /Y "%LINEAGE_TEMP%" "%LINEAGE_FILE%" >NUL',
973
1470
  "if errorlevel 1 goto lineagefailed",
1471
+ 'set "HASH_PATH=%LINEAGE_FILE%"',
1472
+ "call :hashpath",
1473
+ "if errorlevel 1 goto lineagefailed",
1474
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_TRANSACTION_SHA%" goto lineagefailed',
1475
+ 'set "JOURNAL_PHASE=lineage-committed"',
1476
+ "call :writejournal",
1477
+ "if errorlevel 1 goto journalphasefailed",
1478
+ 'set "JOURNAL_PHASE=committed"',
1479
+ 'set "JOURNAL_DECISION=commit"',
1480
+ "call :writejournal",
1481
+ "if errorlevel 1 goto journalcommitfailed",
1482
+ 'set "JOURNAL_ACTION=retire"',
1483
+ "call :writejournal",
1484
+ "if errorlevel 1 goto journalretirefailed",
974
1485
  restart
975
1486
  ? "REM restart deferred until result persistence"
976
1487
  : "REM restart not requested",
@@ -989,8 +1500,36 @@ export function writeWindowsSidecar(ctx) {
989
1500
  ":lineagefailed",
990
1501
  'set "FAIL_STATUS=lineage-write-failed"',
991
1502
  "goto rollbacktransaction",
1503
+ ":journalphasefailed",
1504
+ 'set "FAIL_STATUS=journal-phase-write-failed"',
1505
+ "goto rollbacktransaction",
1506
+ ":journalpreparefailed",
1507
+ 'if exist "%JOURNAL_FILE%" goto journalrecoveryrequired',
1508
+ 'set "RESULT_STATUS=journal-prepare-failed"',
1509
+ "goto cleanup",
1510
+ ":journalcommitfailed",
1511
+ 'set "RESULT_STATUS=journal-commit-write-failed"',
1512
+ "goto journalrecoveryrequired",
1513
+ ":journalretirefailed",
1514
+ 'set "RESULT_STATUS=journal-retirement-failed"',
1515
+ "goto journalrecoveryrequired",
992
1516
  ":rollbacktransaction",
993
- 'if not "%TARGET_COMMITTED%"=="1" goto rollbackalias',
1517
+ 'if "%TARGET_COMMITTED%"=="1" goto rollbackcommittedtarget',
1518
+ 'if exist "%TARGET_EXE%" goto inspectrollbacktarget',
1519
+ 'if "%HAD_TARGET%"=="1" goto targetrollbackfailed',
1520
+ "goto rollbackalias",
1521
+ ":inspectrollbacktarget",
1522
+ 'set "HASH_PATH=%TARGET_EXE%"',
1523
+ "call :hashpath",
1524
+ "if errorlevel 1 goto targetrollbackfailed",
1525
+ 'if /I "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto rollbackalias',
1526
+ 'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto targetrollbackfailed',
1527
+ 'set "TARGET_COMMITTED=1"',
1528
+ ":rollbackcommittedtarget",
1529
+ 'set "HASH_PATH=%TARGET_EXE%"',
1530
+ "call :hashpath",
1531
+ "if errorlevel 1 goto targetrollbackfailed",
1532
+ 'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto targetrollbackfailed',
994
1533
  'copy /B /Y "%TARGET_EXE%" "%FAILED_EXE%" >NUL 2>&1',
995
1534
  'if "%HAD_TARGET%"=="0" goto removefreshcanonical',
996
1535
  'copy /B /Y "%ROLLBACK_SOURCE%" "%ROLLBACK_TEMP%" >NUL',
@@ -1009,7 +1548,23 @@ export function writeWindowsSidecar(ctx) {
1009
1548
  ":targetrollbackfailed",
1010
1549
  'set "ROLLBACK_OK=0"',
1011
1550
  ":rollbackalias",
1012
- 'if not "%ALIAS_COMMITTED%"=="1" goto finishrollback',
1551
+ 'if "%MANAGE_ALIAS%"=="0" goto finishrollback',
1552
+ 'if "%ALIAS_COMMITTED%"=="1" goto rollbackcommittedalias',
1553
+ 'if exist "%ALIAS_EXE%" goto inspectrollbackalias',
1554
+ 'if "%HAD_ALIAS%"=="1" goto aliasrollbackfailed',
1555
+ "goto finishrollback",
1556
+ ":inspectrollbackalias",
1557
+ 'set "HASH_PATH=%ALIAS_EXE%"',
1558
+ "call :hashpath",
1559
+ "if errorlevel 1 goto aliasrollbackfailed",
1560
+ 'if /I "%OBSERVED_SHA%"=="%ALIAS_BEFORE_SHA%" goto finishrollback',
1561
+ 'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto aliasrollbackfailed',
1562
+ 'set "ALIAS_COMMITTED=1"',
1563
+ ":rollbackcommittedalias",
1564
+ 'set "HASH_PATH=%ALIAS_EXE%"',
1565
+ "call :hashpath",
1566
+ "if errorlevel 1 goto aliasrollbackfailed",
1567
+ 'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto aliasrollbackfailed',
1013
1568
  'if "%HAD_ALIAS%"=="0" goto removefreshalias',
1014
1569
  'copy /B /Y "%ALIAS_BACKUP%" "%ALIAS_ROLLBACK%" >NUL',
1015
1570
  "if errorlevel 1 goto aliasrollbackfailed",
@@ -1028,29 +1583,29 @@ export function writeWindowsSidecar(ctx) {
1028
1583
  'set "ROLLBACK_OK=0"',
1029
1584
  ":finishrollback",
1030
1585
  'if "%ROLLBACK_OK%"=="0" goto rollbackfailed',
1031
- ...(operation === "update" && rolledBackLineage
1032
- ? [
1033
- 'if "%BACKUP_COMMITTED%"=="1" goto writerolledbacklineage',
1034
- 'if "%HAD_BACKUP%"=="1" goto rollbackstatecomplete',
1035
- 'move /Y "%BACKUP_TEMP%" "%BACKUP_EXE%" >NUL',
1036
- "if errorlevel 1 goto rollbackstatecomplete",
1037
- 'set "BACKUP_COMMITTED=1"',
1038
- ":writerolledbacklineage",
1039
- `> "%LINEAGE_TEMP%" echo ${JSON.stringify(rolledBackLineage)}`,
1040
- 'move /Y "%LINEAGE_TEMP%" "%LINEAGE_FILE%" >NUL 2>&1',
1041
- "if errorlevel 1 goto rollbacklineagefailed",
1042
- ":rollbackstatecomplete",
1043
- ]
1044
- : []),
1586
+ "call :restorebackupstate",
1587
+ "if errorlevel 1 goto rollbackstatefailed",
1588
+ "call :restorelineagestate",
1589
+ "if errorlevel 1 goto rollbacklineagefailed",
1590
+ 'set "JOURNAL_ACTION=retire"',
1591
+ "call :writejournal",
1592
+ "if errorlevel 1 goto rollbackjournalretirefailed",
1045
1593
  'set "RESULT_STATUS=%FAIL_STATUS%-rolled-back"',
1046
1594
  "goto cleanup",
1047
1595
  ":rollbackfailed",
1048
1596
  'set "RESULT_STATUS=%FAIL_STATUS%-rollback-failed"',
1049
- "goto cleanup",
1597
+ "goto journalrecoveryrequired",
1050
1598
  ":rollbacklineagefailed",
1051
1599
  'set "ROLLBACK_OK=0"',
1052
1600
  'set "FAIL_STATUS=rollback-lineage-write-failed"',
1053
1601
  "goto rollbackfailed",
1602
+ ":rollbackstatefailed",
1603
+ 'set "ROLLBACK_OK=0"',
1604
+ 'set "FAIL_STATUS=rollback-state-restore-failed"',
1605
+ "goto rollbackfailed",
1606
+ ":rollbackjournalretirefailed",
1607
+ 'set "FAIL_STATUS=rollback-journal-retirement-failed"',
1608
+ "goto rollbackfailed",
1054
1609
  ":parenttimeout",
1055
1610
  'set "RESULT_STATUS=parent-timeout"',
1056
1611
  "goto cleanup",
@@ -1069,13 +1624,22 @@ export function writeWindowsSidecar(ctx) {
1069
1624
  "goto cleanup",
1070
1625
  ":stateconflict",
1071
1626
  'set "RESULT_STATUS=prestate-conflict"',
1627
+ 'if exist "%JOURNAL_FILE%" goto journalrecoveryrequired',
1072
1628
  "goto cleanup",
1073
1629
  ":hashfailed",
1074
1630
  'set "RESULT_STATUS=hash-check-failed"',
1631
+ 'if exist "%JOURNAL_FILE%" goto hashfailedafterjournal',
1075
1632
  "goto cleanup",
1633
+ ":hashfailedafterjournal",
1634
+ 'set "FAIL_STATUS=hash-check-failed"',
1635
+ "goto rollbacktransaction",
1076
1636
  ":hashmismatch",
1077
1637
  'set "RESULT_STATUS=sha256-mismatch"',
1638
+ 'if exist "%JOURNAL_FILE%" goto hashmismatchafterjournal',
1078
1639
  "goto cleanup",
1640
+ ":hashmismatchafterjournal",
1641
+ 'set "FAIL_STATUS=sha256-mismatch"',
1642
+ "goto rollbacktransaction",
1079
1643
  ":backupfailed",
1080
1644
  'set "RESULT_STATUS=backup-failed"',
1081
1645
  "goto cleanup",
@@ -1086,23 +1650,35 @@ export function writeWindowsSidecar(ctx) {
1086
1650
  'set "RESULT_STATUS=alias-stage-failed"',
1087
1651
  "goto cleanup",
1088
1652
  ":movefailed",
1089
- 'set "RESULT_STATUS=move-failed"',
1090
- "goto cleanup",
1653
+ 'set "FAIL_STATUS=move-failed"',
1654
+ "goto rollbacktransaction",
1091
1655
  ":locklost",
1092
1656
  'set "RESULT_STATUS=lock-ownership-lost"',
1657
+ "goto journalrecoveryrequired",
1658
+ ":readyfailed",
1093
1659
  'set "WRITE_RESULT=0"',
1660
+ 'set "SKIP_PATH_CLEANUP=1"',
1094
1661
  "goto cleanup",
1095
- ":readyfailed",
1662
+ ":journalrecoveryrequired",
1663
+ 'set "EXIT_CODE=1"',
1096
1664
  'set "WRITE_RESULT=0"',
1097
1665
  'set "SKIP_PATH_CLEANUP=1"',
1666
+ 'set "ROLLBACK_OK=0"',
1098
1667
  "goto cleanup",
1099
1668
  ":cleanup",
1100
1669
  'if "%SKIP_PATH_CLEANUP%"=="1" goto unsafeexit',
1101
- 'if "%ROLLBACK_OK%"=="1" del /F /Q "%BACKUP_TEMP%" >NUL 2>&1',
1102
- 'if "%ROLLBACK_OK%"=="1" del /F /Q "%CURRENT_TEMP%" >NUL 2>&1',
1670
+ 'if exist "%JOURNAL_FILE%" goto cleanupvolatile',
1671
+ 'del /F /Q "%BACKUP_TEMP%" >NUL 2>&1',
1672
+ 'del /F /Q "%CURRENT_TEMP%" >NUL 2>&1',
1673
+ 'del /F /Q "%TARGET_SNAPSHOT%" >NUL 2>&1',
1674
+ 'del /F /Q "%BACKUP_SNAPSHOT%" >NUL 2>&1',
1675
+ 'del /F /Q "%LINEAGE_SNAPSHOT%" >NUL 2>&1',
1676
+ 'if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_BACKUP%" >NUL 2>&1',
1677
+ 'del /F /Q "%JOURNAL_HELPER%" >NUL 2>&1',
1678
+ ":cleanupvolatile",
1103
1679
  'if "%ROLLBACK_OK%"=="1" del /F /Q "%ROLLBACK_TEMP%" >NUL 2>&1',
1680
+ 'if "%ROLLBACK_OK%"=="1" del /F /Q "%BACKUP_ROLLBACK%" >NUL 2>&1',
1104
1681
  'if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_CANDIDATE%" >NUL 2>&1',
1105
- 'if "%ROLLBACK_OK%"=="1" if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_BACKUP%" >NUL 2>&1',
1106
1682
  'if "%ROLLBACK_OK%"=="1" if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_ROLLBACK%" >NUL 2>&1',
1107
1683
  'del /F /Q "%LINEAGE_TEMP%" >NUL 2>&1',
1108
1684
  'del /F /Q "%READY_TEMP%" >NUL 2>&1',
@@ -1125,6 +1701,76 @@ export function writeWindowsSidecar(ctx) {
1125
1701
  'if not errorlevel 1 del /F /Q "%LOCK_FILE%" >NUL 2>&1',
1126
1702
  'if "%EXIT_CODE%"=="0" if "%RESTART_REQUESTED%"=="1" start "" "%TARGET_EXE%"',
1127
1703
  '(goto) 2>NUL & del /F /Q "%~f0" & exit /b %EXIT_CODE%',
1704
+ ":writejournal",
1705
+ '"%CC_SYSTEM_ROOT%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%JOURNAL_HELPER%" >NUL 2>&1',
1706
+ "exit /b %ERRORLEVEL%",
1707
+ ":restorebackupstate",
1708
+ 'if "%HAD_BACKUP%"=="0" goto restorebackupabsent',
1709
+ 'if not exist "%BACKUP_EXE%" goto restorebackupfromsnapshot',
1710
+ 'set "HASH_PATH=%BACKUP_EXE%"',
1711
+ "call :hashpath",
1712
+ "if errorlevel 1 exit /b 1",
1713
+ 'if /I "%OBSERVED_SHA%"=="%BACKUP_BEFORE_SHA%" exit /b 0',
1714
+ 'if "%OPERATION%"=="rescue" exit /b 1',
1715
+ 'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" exit /b 1',
1716
+ ":restorebackupfromsnapshot",
1717
+ 'if not exist "%BACKUP_SNAPSHOT%" exit /b 1',
1718
+ 'set "HASH_PATH=%BACKUP_SNAPSHOT%"',
1719
+ "call :hashpath",
1720
+ "if errorlevel 1 exit /b 1",
1721
+ 'if /I not "%OBSERVED_SHA%"=="%BACKUP_BEFORE_SHA%" exit /b 1',
1722
+ 'copy /B /Y "%BACKUP_SNAPSHOT%" "%BACKUP_ROLLBACK%" >NUL',
1723
+ "if errorlevel 1 exit /b 1",
1724
+ 'move /Y "%BACKUP_ROLLBACK%" "%BACKUP_EXE%" >NUL',
1725
+ "if errorlevel 1 exit /b 1",
1726
+ 'set "HASH_PATH=%BACKUP_EXE%"',
1727
+ "call :hashpath",
1728
+ "if errorlevel 1 exit /b 1",
1729
+ 'if /I not "%OBSERVED_SHA%"=="%BACKUP_BEFORE_SHA%" exit /b 1',
1730
+ "exit /b 0",
1731
+ ":restorebackupabsent",
1732
+ 'if not exist "%BACKUP_EXE%" exit /b 0',
1733
+ 'if "%OPERATION%"=="rescue" exit /b 1',
1734
+ 'if "%HAD_TARGET%"=="0" exit /b 1',
1735
+ 'set "HASH_PATH=%BACKUP_EXE%"',
1736
+ "call :hashpath",
1737
+ "if errorlevel 1 exit /b 1",
1738
+ 'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" exit /b 1',
1739
+ 'del /F /Q "%BACKUP_EXE%" >NUL 2>&1',
1740
+ 'if exist "%BACKUP_EXE%" exit /b 1',
1741
+ "exit /b 0",
1742
+ ":restorelineagestate",
1743
+ 'if "%HAD_LINEAGE%"=="0" goto restorelineageabsent',
1744
+ 'if not exist "%LINEAGE_FILE%" goto restorelineagefromsnapshot',
1745
+ 'set "HASH_PATH=%LINEAGE_FILE%"',
1746
+ "call :hashpath",
1747
+ "if errorlevel 1 exit /b 1",
1748
+ 'if /I "%OBSERVED_SHA%"=="%LINEAGE_BEFORE_SHA%" exit /b 0',
1749
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_TRANSACTION_SHA%" exit /b 1',
1750
+ ":restorelineagefromsnapshot",
1751
+ 'if not exist "%LINEAGE_SNAPSHOT%" exit /b 1',
1752
+ 'set "HASH_PATH=%LINEAGE_SNAPSHOT%"',
1753
+ "call :hashpath",
1754
+ "if errorlevel 1 exit /b 1",
1755
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_BEFORE_SHA%" exit /b 1',
1756
+ 'copy /B /Y "%LINEAGE_SNAPSHOT%" "%LINEAGE_TEMP%" >NUL',
1757
+ "if errorlevel 1 exit /b 1",
1758
+ 'move /Y "%LINEAGE_TEMP%" "%LINEAGE_FILE%" >NUL',
1759
+ "if errorlevel 1 exit /b 1",
1760
+ 'set "HASH_PATH=%LINEAGE_FILE%"',
1761
+ "call :hashpath",
1762
+ "if errorlevel 1 exit /b 1",
1763
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_BEFORE_SHA%" exit /b 1',
1764
+ "exit /b 0",
1765
+ ":restorelineageabsent",
1766
+ 'if not exist "%LINEAGE_FILE%" exit /b 0',
1767
+ 'set "HASH_PATH=%LINEAGE_FILE%"',
1768
+ "call :hashpath",
1769
+ "if errorlevel 1 exit /b 1",
1770
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_TRANSACTION_SHA%" exit /b 1',
1771
+ 'del /F /Q "%LINEAGE_FILE%" >NUL 2>&1',
1772
+ 'if exist "%LINEAGE_FILE%" exit /b 1',
1773
+ "exit /b 0",
1128
1774
  ":hashpath",
1129
1775
  'set "OBSERVED_SHA="',
1130
1776
  'for /f "usebackq delims=" %%H in (`%CC_SYSTEM_ROOT%\\System32\\certutil.exe -hashfile "%HASH_PATH%" SHA256 ^| %CC_SYSTEM_ROOT%\\System32\\findstr.exe /R /X /I "[0-9A-F][0-9A-F]*"`) do if not defined OBSERVED_SHA set "OBSERVED_SHA=%%H"',
@@ -1160,21 +1806,46 @@ export function writeWindowsSidecar(ctx) {
1160
1806
  ":prestate_backup",
1161
1807
  'if "%HAD_BACKUP%"=="1" goto prestate_backup_present',
1162
1808
  'if exist "%BACKUP_EXE%" exit /b 1',
1163
- "exit /b 0",
1809
+ "goto prestate_lineage",
1164
1810
  ":prestate_backup_present",
1165
1811
  'if not exist "%BACKUP_EXE%" exit /b 1',
1166
1812
  'set "HASH_PATH=%BACKUP_EXE%"',
1167
1813
  "call :hashpath",
1168
1814
  "if errorlevel 1 exit /b 1",
1169
1815
  'if /I not "%OBSERVED_SHA%"=="%BACKUP_BEFORE_SHA%" exit /b 1',
1816
+ ":prestate_lineage",
1817
+ 'if "%HAD_LINEAGE%"=="1" goto prestate_lineage_present',
1818
+ 'if exist "%LINEAGE_FILE%" exit /b 1',
1819
+ "exit /b 0",
1820
+ ":prestate_lineage_present",
1821
+ 'if not exist "%LINEAGE_FILE%" exit /b 1',
1822
+ 'set "HASH_PATH=%LINEAGE_FILE%"',
1823
+ "call :hashpath",
1824
+ "if errorlevel 1 exit /b 1",
1825
+ 'if /I not "%OBSERVED_SHA%"=="%LINEAGE_BEFORE_SHA%" exit /b 1',
1170
1826
  "exit /b 0",
1171
1827
  ].join("\r\n");
1172
1828
 
1173
- fs.writeFileSync(sidecarPath, `\uFEFF${cmd}`, {
1174
- encoding: "utf8",
1175
- flag: "wx",
1176
- mode: 0o600,
1177
- });
1829
+ try {
1830
+ fs.writeFileSync(
1831
+ journalHelperPath,
1832
+ windowsGenerationJournalHelperSource(),
1833
+ {
1834
+ encoding: "utf8",
1835
+ flag: "wx",
1836
+ mode: 0o600,
1837
+ },
1838
+ );
1839
+ fs.writeFileSync(sidecarPath, `\uFEFF${cmd}`, {
1840
+ encoding: "utf8",
1841
+ flag: "wx",
1842
+ mode: 0o600,
1843
+ });
1844
+ } catch (error) {
1845
+ removeRegularFileIfPresent(sidecarPath);
1846
+ removeRegularFileIfPresent(journalHelperPath);
1847
+ throw error;
1848
+ }
1178
1849
  return sidecarPath;
1179
1850
  }
1180
1851
 
@@ -1219,12 +1890,27 @@ export async function rollbackLastKnownGood(ctx = {}) {
1219
1890
  "BACKUP_NOT_SIBLING",
1220
1891
  );
1221
1892
  }
1893
+ if (
1894
+ platform === "win32" &&
1895
+ !samePath(backupPath, statePaths.backupPath, platform)
1896
+ ) {
1897
+ throw new ApplyError(
1898
+ "Windows restart recovery requires the canonical .previous backup",
1899
+ "BACKUP_NOT_CANONICAL",
1900
+ );
1901
+ }
1222
1902
  assertApplyPath(backupPath, "last-known-good backup", false, platform);
1223
1903
  assertApplyPath(statePaths.lineagePath, "update lineage", false, platform);
1224
1904
  assertApplyPath(layout.canonicalPath, "canonical executable", true, platform);
1225
1905
  if (layout.aliasPath) {
1226
1906
  assertApplyPath(layout.aliasPath, "managed CLI alias", true, platform);
1227
1907
  }
1908
+ if (lstatOrNull(statePaths.journalPath)) {
1909
+ throw new ApplyError(
1910
+ `an interrupted native generation requires restart recovery: ${statePaths.journalPath}`,
1911
+ "RECOVERY_REQUIRED",
1912
+ );
1913
+ }
1228
1914
 
1229
1915
  const updateLock = acquireUpdateLock(statePaths.lockPath, platform);
1230
1916
  const transactionId = crypto.randomUUID();
@@ -1263,6 +1949,14 @@ export async function rollbackLastKnownGood(ctx = {}) {
1263
1949
  lineage.previousSha256,
1264
1950
  "last-known-good backup",
1265
1951
  );
1952
+ const hadPriorBackup = Boolean(lstatOrNull(statePaths.backupPath));
1953
+ const priorBackupSha = hadPriorBackup
1954
+ ? stableSha256(statePaths.backupPath, "existing native backup")
1955
+ : null;
1956
+ const priorLineageSha = stableSha256(
1957
+ statePaths.lineagePath,
1958
+ "native update lineage before rescue",
1959
+ );
1266
1960
 
1267
1961
  rescueStagingPath = uniqueSibling(layout.canonicalPath, "rescue");
1268
1962
  atomicCopyFile(backupPath, rescueStagingPath, {
@@ -1293,8 +1987,10 @@ export async function rollbackLastKnownGood(ctx = {}) {
1293
1987
  aliasBeforeSha,
1294
1988
  hadTarget,
1295
1989
  hadAlias,
1296
- hadPriorBackup: true,
1297
- priorBackupSha: lineage.previousSha256,
1990
+ hadPriorBackup,
1991
+ hadPriorLineage: true,
1992
+ priorBackupSha,
1993
+ priorLineageSha,
1298
1994
  parentPid,
1299
1995
  restart: Boolean(restart),
1300
1996
  verify: Boolean(verify),
@@ -1314,6 +2010,9 @@ export async function rollbackLastKnownGood(ctx = {}) {
1314
2010
  rescueSha: lineage.previousSha256,
1315
2011
  hadTarget,
1316
2012
  targetBeforeSha,
2013
+ hadPriorBackup,
2014
+ priorBackupSha,
2015
+ priorLineageSha,
1317
2016
  restart: Boolean(restart),
1318
2017
  verify: Boolean(verify),
1319
2018
  spawnImpl,
@@ -1342,22 +2041,89 @@ function applyPosixRescue(ctx) {
1342
2041
  rescueSha,
1343
2042
  hadTarget,
1344
2043
  targetBeforeSha,
2044
+ hadPriorBackup,
2045
+ priorBackupSha,
2046
+ priorLineageSha,
1345
2047
  restart,
1346
2048
  verify,
1347
2049
  spawnImpl,
1348
2050
  verifyImpl,
1349
2051
  } = ctx;
1350
2052
  const targetPath = layout.canonicalPath;
1351
- const currentSnapshotPath = uniqueSibling(targetPath, "rescue-current");
1352
- if (hadTarget) {
1353
- atomicCopyFile(targetPath, currentSnapshotPath, {
1354
- preserveModeFrom: targetPath,
2053
+ const snapshotPaths = generationSnapshotPaths(targetPath, transactionId);
2054
+ const journal = makeGenerationJournal({
2055
+ transactionId,
2056
+ operation: "rescue",
2057
+ expectedSha: rescueSha,
2058
+ hadTarget,
2059
+ targetBeforeSha,
2060
+ hadBackup: hadPriorBackup,
2061
+ backupBeforeSha: priorBackupSha,
2062
+ hadLineage: true,
2063
+ lineageBeforeSha: priorLineageSha,
2064
+ });
2065
+
2066
+ try {
2067
+ if (hadTarget) {
2068
+ atomicCopyFile(targetPath, snapshotPaths.priorTargetPath, {
2069
+ preserveModeFrom: targetPath,
2070
+ updateLock,
2071
+ });
2072
+ assertExpectedFileHash(
2073
+ snapshotPaths.priorTargetPath,
2074
+ targetBeforeSha,
2075
+ "pre-rescue target generation snapshot",
2076
+ );
2077
+ }
2078
+ if (hadPriorBackup) {
2079
+ atomicCopyFile(statePaths.backupPath, snapshotPaths.priorBackupPath, {
2080
+ preserveModeFrom: statePaths.backupPath,
2081
+ updateLock,
2082
+ });
2083
+ assertExpectedFileHash(
2084
+ snapshotPaths.priorBackupPath,
2085
+ priorBackupSha,
2086
+ "pre-rescue backup generation snapshot",
2087
+ );
2088
+ }
2089
+ atomicCopyFile(statePaths.lineagePath, snapshotPaths.priorLineagePath, {
2090
+ preserveModeFrom: statePaths.lineagePath,
1355
2091
  updateLock,
1356
2092
  });
1357
2093
  assertExpectedFileHash(
1358
- currentSnapshotPath,
1359
- targetBeforeSha,
1360
- "pre-rescue executable",
2094
+ snapshotPaths.priorLineagePath,
2095
+ priorLineageSha,
2096
+ "pre-rescue lineage generation snapshot",
2097
+ );
2098
+ } catch (error) {
2099
+ cleanupGenerationSnapshots(snapshotPaths);
2100
+ throw asApplyError(
2101
+ error,
2102
+ "GENERATION_SNAPSHOT_FAILED",
2103
+ "could not preserve the native rescue pre-state",
2104
+ );
2105
+ }
2106
+
2107
+ try {
2108
+ writeGenerationJournalPhase(
2109
+ statePaths.journalPath,
2110
+ journal,
2111
+ "prepared",
2112
+ "rollback",
2113
+ updateLock,
2114
+ );
2115
+ } catch (error) {
2116
+ if (lstatOrNull(statePaths.journalPath)) {
2117
+ throw new ApplyError(
2118
+ `native rescue intent persistence is unknown: ${error.message}`,
2119
+ "RESCUE_ROLLBACK_FAILED",
2120
+ );
2121
+ }
2122
+ cleanupGenerationSnapshots(snapshotPaths);
2123
+ throw asApplyError(
2124
+ error,
2125
+ "JOURNAL_WRITE_FAILED",
2126
+ "could not persist native rescue intent",
1361
2127
  );
1362
2128
  }
1363
2129
 
@@ -1377,8 +2143,35 @@ function applyPosixRescue(ctx) {
1377
2143
  assertUpdateLockOwned(updateLock);
1378
2144
  fs.renameSync(rescueStagingPath, targetPath);
1379
2145
  fsyncDirectory(path.dirname(targetPath));
2146
+ writeGenerationJournalPhase(
2147
+ statePaths.journalPath,
2148
+ journal,
2149
+ "target-committed",
2150
+ "rollback",
2151
+ updateLock,
2152
+ );
1380
2153
  } catch (error) {
1381
- removeRegularFileIfPresent(currentSnapshotPath);
2154
+ try {
2155
+ rollbackFailedPosixUpdate({
2156
+ targetPath,
2157
+ statePaths,
2158
+ transactionId,
2159
+ hadTarget,
2160
+ targetBeforeSha,
2161
+ hadPriorBackup,
2162
+ hadPriorLineage: true,
2163
+ priorBackupSha,
2164
+ priorLineageSha,
2165
+ snapshotPaths,
2166
+ journal,
2167
+ updateLock,
2168
+ });
2169
+ } catch (recoveryError) {
2170
+ throw new ApplyError(
2171
+ `rescue target commit failed and its generation journal could not be retired: ${recoveryError.message}`,
2172
+ "RESCUE_ROLLBACK_FAILED",
2173
+ );
2174
+ }
1382
2175
  throw new ApplyError(
1383
2176
  `rescue rename failed: ${error.message}`,
1384
2177
  "RESCUE_RENAME_FAILED",
@@ -1401,6 +2194,22 @@ function applyPosixRescue(ctx) {
1401
2194
  if (!failure && verify) {
1402
2195
  failure = verificationFailure(targetPath, verifyImpl);
1403
2196
  }
2197
+ if (!failure) {
2198
+ try {
2199
+ writeGenerationJournalPhase(
2200
+ statePaths.journalPath,
2201
+ journal,
2202
+ "verified",
2203
+ "rollback",
2204
+ updateLock,
2205
+ );
2206
+ } catch (error) {
2207
+ failure = new ApplyError(
2208
+ `could not persist native rescue verification: ${error.message}`,
2209
+ "JOURNAL_WRITE_FAILED",
2210
+ );
2211
+ }
2212
+ }
1404
2213
  if (!failure) {
1405
2214
  try {
1406
2215
  atomicWriteJson(
@@ -1413,6 +2222,13 @@ function applyPosixRescue(ctx) {
1413
2222
  }),
1414
2223
  updateLock,
1415
2224
  );
2225
+ writeGenerationJournalPhase(
2226
+ statePaths.journalPath,
2227
+ journal,
2228
+ "lineage-committed",
2229
+ "rollback",
2230
+ updateLock,
2231
+ );
1416
2232
  } catch (error) {
1417
2233
  failure = new ApplyError(
1418
2234
  `could not persist rescue lineage: ${error.message}`,
@@ -1422,39 +2238,53 @@ function applyPosixRescue(ctx) {
1422
2238
  }
1423
2239
 
1424
2240
  if (failure) {
1425
- let restored = false;
1426
2241
  try {
1427
- if (hadTarget) {
1428
- atomicCopyFile(currentSnapshotPath, targetPath, {
1429
- preserveModeFrom: currentSnapshotPath,
1430
- updateLock,
1431
- });
1432
- assertExpectedFileHash(
1433
- targetPath,
1434
- targetBeforeSha,
1435
- "restored pre-rescue executable",
1436
- );
1437
- } else {
1438
- assertUpdateLockOwned(updateLock);
1439
- fs.renameSync(targetPath, uniqueSibling(targetPath, "failed-rescue"));
1440
- }
1441
- restored = true;
2242
+ rollbackFailedPosixUpdate({
2243
+ targetPath,
2244
+ statePaths,
2245
+ transactionId,
2246
+ hadTarget,
2247
+ targetBeforeSha,
2248
+ hadPriorBackup,
2249
+ hadPriorLineage: true,
2250
+ priorBackupSha,
2251
+ priorLineageSha,
2252
+ snapshotPaths,
2253
+ journal,
2254
+ updateLock,
2255
+ });
1442
2256
  } catch (error) {
1443
2257
  throw new ApplyError(
1444
- `rescue verification failed and pre-rescue restore failed; recovery snapshot preserved at ${currentSnapshotPath}: ${error.message}`,
2258
+ `rescue verification failed and pre-rescue restoration failed: ${error.message}`,
1445
2259
  "RESCUE_ROLLBACK_FAILED",
1446
2260
  );
1447
- } finally {
1448
- if (restored) {
1449
- assertUpdateLockOwned(updateLock);
1450
- removeRegularFileIfPresent(currentSnapshotPath);
1451
- }
1452
2261
  }
1453
2262
  throw failure;
1454
2263
  }
1455
2264
 
1456
- assertUpdateLockOwned(updateLock);
1457
- removeRegularFileIfPresent(currentSnapshotPath);
2265
+ try {
2266
+ writeGenerationJournalPhase(
2267
+ statePaths.journalPath,
2268
+ journal,
2269
+ "committed",
2270
+ "commit",
2271
+ updateLock,
2272
+ );
2273
+ } catch (error) {
2274
+ throw new ApplyError(
2275
+ `native rescue commit decision is unknown: ${error.message}`,
2276
+ "RESCUE_ROLLBACK_FAILED",
2277
+ );
2278
+ }
2279
+ try {
2280
+ retireGenerationJournal(statePaths, journal, updateLock);
2281
+ } catch (error) {
2282
+ throw new ApplyError(
2283
+ `native rescue committed but journal retirement failed: ${error.message}`,
2284
+ "RESCUE_ROLLBACK_FAILED",
2285
+ );
2286
+ }
2287
+ cleanupGenerationSnapshots(snapshotPaths);
1458
2288
  restartVerifiedExecutable(targetPath, restart, spawnImpl);
1459
2289
  return buildPlan({
1460
2290
  platform: "posix",
@@ -1485,6 +2315,91 @@ function makeLineage({
1485
2315
  };
1486
2316
  }
1487
2317
 
2318
+ function generationSnapshotPaths(targetPath, transactionId) {
2319
+ const directory = path.dirname(targetPath);
2320
+ return {
2321
+ priorTargetPath: path.join(
2322
+ directory,
2323
+ `.chainlesschain.target-prior-${transactionId}`,
2324
+ ),
2325
+ priorBackupPath: path.join(
2326
+ directory,
2327
+ `.chainlesschain.backup-prior-${transactionId}`,
2328
+ ),
2329
+ priorLineagePath: path.join(
2330
+ directory,
2331
+ `.chainlesschain.lineage-prior-${transactionId}`,
2332
+ ),
2333
+ };
2334
+ }
2335
+
2336
+ function makeGenerationJournal({
2337
+ transactionId,
2338
+ operation,
2339
+ expectedSha,
2340
+ hadTarget,
2341
+ targetBeforeSha,
2342
+ hadBackup,
2343
+ backupBeforeSha,
2344
+ hadLineage,
2345
+ lineageBeforeSha,
2346
+ }) {
2347
+ return {
2348
+ schema: NATIVE_GENERATION_TRANSACTION_SCHEMA,
2349
+ transactionId,
2350
+ operation,
2351
+ phase: "prepared",
2352
+ decision: "rollback",
2353
+ expectedSha256: expectedSha,
2354
+ hadTarget,
2355
+ targetBeforeSha256: hadTarget ? targetBeforeSha : null,
2356
+ hadBackup,
2357
+ backupBeforeSha256: hadBackup ? backupBeforeSha : null,
2358
+ hadAlias: false,
2359
+ aliasBeforeSha256: null,
2360
+ hadLineage,
2361
+ lineageBeforeSha256: hadLineage ? lineageBeforeSha : null,
2362
+ updatedAt: new Date().toISOString(),
2363
+ };
2364
+ }
2365
+
2366
+ function writeGenerationJournalPhase(
2367
+ journalPath,
2368
+ journal,
2369
+ phase,
2370
+ decision,
2371
+ updateLock,
2372
+ ) {
2373
+ journal.phase = phase;
2374
+ journal.decision = decision;
2375
+ journal.updatedAt = new Date().toISOString();
2376
+ atomicWriteJson(journalPath, journal, updateLock);
2377
+ }
2378
+
2379
+ function retireGenerationJournal(statePaths, journal, updateLock) {
2380
+ assertUpdateLockOwned(updateLock);
2381
+ const current = JSON.parse(fs.readFileSync(statePaths.journalPath, "utf8"));
2382
+ if (
2383
+ current.schema !== NATIVE_GENERATION_TRANSACTION_SCHEMA ||
2384
+ current.transactionId !== journal.transactionId ||
2385
+ current.decision !== journal.decision ||
2386
+ (current.phase === "committed") !== (current.decision === "commit")
2387
+ ) {
2388
+ throw new ApplyError(
2389
+ "native generation journal changed before retirement",
2390
+ "JOURNAL_CHANGED",
2391
+ );
2392
+ }
2393
+ fs.renameSync(statePaths.journalPath, statePaths.lastJournalPath);
2394
+ fsyncDirectory(path.dirname(statePaths.journalPath));
2395
+ }
2396
+
2397
+ function cleanupGenerationSnapshots(snapshotPaths) {
2398
+ for (const snapshotPath of Object.values(snapshotPaths)) {
2399
+ removeRegularFileIfPresent(snapshotPath);
2400
+ }
2401
+ }
2402
+
1488
2403
  function buildPlan({
1489
2404
  platform,
1490
2405
  action,
@@ -1595,6 +2510,16 @@ function sameDirectory(firstPath, secondPath, platform = process.platform) {
1595
2510
  return first === second;
1596
2511
  }
1597
2512
 
2513
+ function samePath(firstPath, secondPath, platform = process.platform) {
2514
+ let first = path.resolve(firstPath);
2515
+ let second = path.resolve(secondPath);
2516
+ if (platform === "win32") {
2517
+ first = first.toLowerCase();
2518
+ second = second.toLowerCase();
2519
+ }
2520
+ return first === second;
2521
+ }
2522
+
1598
2523
  function assertApplyPath(filePath, label, allowMissingLeaf, platform) {
1599
2524
  try {
1600
2525
  assertStateSafeRegularFile(filePath, {