chainlesschain 0.166.34 → 0.166.36

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 (169) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-D8PJmSrh.js → AIOps-Zs-zPwVG.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DG6DqHA3.js → ActionButton-HZlxM6H0.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DiLleeWs.js → Analytics-CXyThRDb.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-DxYkhlHK.js → AppLayout-DeyYh_EF.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-BAW3Kdsi.js → Artifacts-BQDtMwkx.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DBr4uY6E.js → Audit-DNH4UOux.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-Dw0r-bHX.js → BackgroundAgents-BkKdIxE6.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-AyhwYsio.js → Backup-DYy4aJom.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Cj8qJotr.js → BaseInput-DjoyGVYm.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DcvimQ0u.js → Chat-D86ZYgOD.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer--joJ1e2f.js → ChatBubbleRenderer-RxI_YYth.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-C2mlU77l.js → Checkbox-C7p8UY99.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CAfc3YaV.js → Codegen-DnapqgV5.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CIxoxtJI.js → Col-C29J2DBT.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DvufZoRg.js → Community-Bf3KeLCr.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-55JTa99J.js → Compact-DA83wB48.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-BllWog6P.js → Compliance-CXXVgRFG.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-ThsFGc5p.js → Cowork-B28Ftuqv.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-CZjl5Ig2.js → Cron-CO-BcnDf.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DIAKAoQT.js → Crosschain-BmgPMFCs.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-ayygwEil.js → DID-CYW9WslA.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C3Xjm2UO.js → Dashboard-CWWhVWds.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-C1S3zf-j.js → Dropdown-CjTuc1-n.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DRvriU-H.js → EmailListRenderer-BrROAu4d.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-iSX2bmsD.js → FamilyGuardDashboard-Ci8Lmk8W.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-Dk0Stkv-.js → Federation-CKzKVLE_.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DasXSGff.js → FormItemContext-CC2CK5PQ.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-Dv9XGlxI.js → GenericCardRenderer-BYa3ElyT.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-B5PgXL3Z.js → Git-Bmn3ucA4.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-B78x11e_.js → Governance-DqEOXitg.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-b7CX1RqW.js → Inference-BjfeExba.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph--MgBzMVp.js → KnowledgeGraph-CBTyIFlJ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CYCiGmVc.js → Logs-KZfA_M9Q.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-yrl10SWw.js → MarkdownRenderer-BG7vi3V4.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-CsbwQ0eM.js → Marketplace-BFeK4he0.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-0GBWHVra.js → McpTools-Brg7uUoK.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-fAywmAYr.js → Memory-Ce4AmyX3.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BeNEyy3s.js → MobileBridge-Dx1VBL_E.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-DPyRr2Xl.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-CqRhJB70.js → Mtc-BzRmtDQU.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-BEyGZflc.js → MtcAudit-DvZ5kLvJ.js} +4 -4
  43. package/src/assets/web-panel/assets/{Multisig-luz3EBT2.js → Multisig-D8uGUjX1.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-Dkbvp8IX.js → NLProgramming-igc-TQE5.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Cf9I2K8j.js → Notes-CTqDYlek.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BTfuzynL.js → NotificationSettings-CkcLa2rb.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BfScYvnH.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-BbG3-vDy.js → Organization-BjKK9SdB.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-CMtFKEIQ.js → Overflow-BkcghTPE.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-6SAEb_lU.js → P2P-l9f34Xu8.js} +2 -2
  51. package/src/assets/web-panel/assets/PdhVaultBrowser-yVXWmwWe.js +7 -0
  52. package/src/assets/web-panel/assets/{Permissions-ClZxZKZl.js → Permissions-DeSYs0VV.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BYkyoZDT.js → PersonalDataHub-CmKmhy0q.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-B28ZgADI.js → Pipeline-DBxS4R_r.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BxtZywbB.js → Privacy-CKngdGw3.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-DPqoRnxK.js → ProjectInit-0Pso0jXN.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-QjKvMkau.js → ProjectSettings-BHrqWQ6r.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-Cp0Z8ZG0.js → Projects-B8wIh9PK.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-CrS-rbyb.js → Providers-CXFTOHRV.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-xTrhbVAU.js → QrScannerModal-DfN0c_BN.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-DFOqJoq-.js → QuickAsk-v1Z5hty_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DXlKqu2-.js → Recommend-DOaVmZUi.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BLLaZH8K.js → RemoteSession--EWcPBY1.js} +3 -3
  64. package/src/assets/web-panel/assets/{Reputation-CUgAdJ11.js → Reputation-DUWFSNvV.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DzOO3wP-.js → Row-0-bFnXWF.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B6GrRvh8.js → RssFeed-B_uTfubW.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-Cj3zKLb3.js → Search-CqxiTLfB.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-LnktzidI.js → Security-CGoHOz5B.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-BQ6sIc2K.js → Services-COhAbS45.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-CCSV3Jbw.js → Skeleton-B97crp6v.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CpoQ4SNk.js → Skills-er_XdavV.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DbAbgRPq.js → Sla-RpkWkzOL.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-DDVXmT5y.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-BKFzjN9Z.js → SyncSettings-Bbiw6ohw.js} +2 -2
  75. package/src/assets/web-panel/assets/Tasks-CRghr7lo.js +1 -0
  76. package/src/assets/web-panel/assets/{Templates-DTEQfFAQ.js → Templates-BUQkfydG.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-wToJWKXB.js → Tenant-CfUFOsZu.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-1s3c7kmF.js → Terminal-BGY7gi4M.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BcfsYW3u.js → TimelineRenderer-B3Len0l7.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-Byez6FY0.js → Tokens-BjeHn2zI.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Cq8mZO88.js → Trigger-ctcZ2mcv.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DUrMmEwR.js → Trust-oncF3TpO.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CVHwXrVU.js → UkeySign-BwKbVYCd.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-CDKbTRUS.js → VideoEditing-D1dgL-3U.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BqnEBIOy.js → Wallet-4hXAfjBL.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-CeTMpj7_.js → WebAuthn-C8p3cotb.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-F6OgjV3C.js → WorkflowEditor-BnrjG77E.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CMHUCwq3.js → chat-B-YNK7EI.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-DzVM_fpJ.js → colors-B9HYvK-e.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-Hhmg51yT.js → compact-item-BJr0quBA.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-SwHt7x3Q.js → createContext-CXfbQEWD.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DNpXMZaQ.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-HGNKkn9A.js → hasIn-B7Q02hdZ.js} +1 -1
  94. package/src/assets/web-panel/assets/index-2hcdmWpb.js +1 -0
  95. package/src/assets/web-panel/assets/{index-DQT5thlV.js → index-4gZEpDAG.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CWkWh48N.js → index-6wWd08X-.js} +1 -1
  97. package/src/assets/web-panel/assets/{index--iOqM_AI.js → index-B6h86qjh.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-pwLuJGFh.js → index-BB4KIBso.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-COOcGOyK.js → index-BClzZrmx.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BRikmONJ.js → index-BNL9lmWL.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C-YkiVyL.js → index-BU_adeUp.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BmPYmoY3.js → index-Bd3nX6aj.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BstLYy9q.js → index-Bf0OxvOh.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BlHB5eax.js → index-BleXwr2w.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Ba2Kszdy.js → index-BmCD1_HC.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Bq9MMKno.js → index-BmvpVSmM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BJqNTF1S.js → index-BoNVnnAq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-MDdTkUpA.js → index-BtunVZuS.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-jrRtmiUh.js → index-BvRqE3Un.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Co4RcBua.js → index-C3M8M9_U.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DgoefbLs.js → index-C7IZWdoB.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-F7Grz3-G.js → index-CEujQqdl.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-oz0Xxs8D.js → index-COdAugDt.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-B6YQN5_R.js → index-CQ7qYZS4.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BDhCx17_.js → index-CU7cBzr9.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CMG_2fK2.js → index-CXgHZ_HG.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-sYkFp9A4.js → index-CcyxVqzn.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DyZUrnje.js → index-D0zuXz4s.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C1QJxiOL.js → index-D1GP2MQz.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BDE-WiE5.js → index-D5g5_oua.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-D5dcWBqg.js → index-DMoUa3dF.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CDtzUt75.js → index-DPUg6cVc.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Dcg7nQDe.js → index-D_UneDq3.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DbQCUG4D.js +1 -0
  125. package/src/assets/web-panel/assets/{index-Cc-d6f0F.js → index-DsV1HA0Z.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B_HgLPC5.js → index-DutYFyC0.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BDt4wSl6.js → index-G7Z_On_n.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-E-bI8HpV.js → index-Gjegl5Ig.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Cnm4zroJ.js → index-HzCaU25O.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-AQcDW8bB.js → index-cq5Q-RHa.js} +3 -3
  131. package/src/assets/web-panel/assets/{index-aKV4Py_2.js → index-iCKFl_jt.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-ChLHLBkF.js → index-kAggAkrB.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-C_2QM45F.js → initDefaultProps-DrzGM6fO.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-DkxY04tR.js → motion-SE_xFnGL.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-D0CJlA7D.js → move-CrPjKNiG.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DjP38YUD.js → mtc-parser-CpcyFPK6.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-C9CJDaGT.js → omit-BV0g7vf4.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-D5Wq2fa8.js → pickAttrs-lWpsc-k6.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-DJ6Dixs7.js → placementArrow-CiewvU4X.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DMmM0Dee.js → responsiveObserve-DbF8pamO.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-DipmpYA-.js → slide-Bn82Ip9-.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-DSqw6_nU.js → statusUtils-J76gk_-G.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-t828MYjI.js → styleChecker-BL95J74a.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-COsSNzDQ.js → useFlexGapSupport-BUzhdQsv.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-Bq5IEzv_.js → useFs-B2Fppepb.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CUKM0HdW.js → usePersonalDataHub-DjTASfYE.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-l-JwAt2M.js → vnode-BwrOgBqP.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-D7xkW-gD.js → zoom-B6MeT_Gw.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/llm.js +4 -3
  151. package/src/data/changelog.json +26 -0
  152. package/src/lib/evolution/evolution-artifact-ports.js +40 -3
  153. package/src/lib/evolution/evolution-file-identity.js +50 -0
  154. package/src/lib/evolution/evolution-file-witness.js +206 -91
  155. package/src/lib/evolution/evolution-ledger-file-backend.js +4 -0
  156. package/src/lib/evolution/evolution-ledger.js +37 -5
  157. package/src/lib/evolution/evolution-witness-segments.js +330 -0
  158. package/src/lib/evolution/progressive-canary-external-watchdog.js +6 -4
  159. package/src/lib/evolution/skill-candidate-registry.js +45 -11
  160. package/src/lib/evolution/skill-release-registry.js +46 -12
  161. package/src/lib/llm-connection-probe.js +21 -4
  162. package/src/assets/web-panel/assets/MobileProjects-Zt1cpErd.js +0 -1
  163. package/src/assets/web-panel/assets/OrderTableRenderer-DBmCtl2H.js +0 -1
  164. package/src/assets/web-panel/assets/PdhVaultBrowser-CWG2OLvh.js +0 -7
  165. package/src/assets/web-panel/assets/SpeechSettings-C4cJ_hoD.js +0 -1
  166. package/src/assets/web-panel/assets/Tasks-Cf4bl7NS.js +0 -1
  167. package/src/assets/web-panel/assets/devWarning-5q_Y0E88.js +0 -1
  168. package/src/assets/web-panel/assets/index-dl9TJMh5.js +0 -1
  169. package/src/assets/web-panel/assets/index-x0DDgUaT.js +0 -1
@@ -0,0 +1,330 @@
1
+ import path from "node:path";
2
+ import { ensurePrivateDirectory } from "../secure-fs.js";
3
+
4
+ export const EVOLUTION_SEGMENTED_WITNESS_SCHEMA =
5
+ "chainlesschain.evolution-file-witness-store/v2";
6
+ const SEGMENT_SCHEMA = "chainlesschain.evolution-file-witness-segment/v1";
7
+ export const EVOLUTION_WITNESS_SEGMENT_RECORDS = 256;
8
+ const MAX_RECORDS = 262_144;
9
+ const MAX_SEGMENTS = MAX_RECORDS / EVOLUTION_WITNESS_SEGMENT_RECORDS;
10
+ const MAX_CACHE_BYTES = 16 * 1024 * 1024;
11
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
12
+ const HEAD_KEYS = new Set([
13
+ "schema",
14
+ "current",
15
+ "segments",
16
+ "history",
17
+ "discardedAnchors",
18
+ ]);
19
+ const SEGMENT_KEYS = new Set(["schema", "history", "discardedAnchors"]);
20
+
21
+ function exact(value, keys, label) {
22
+ if (
23
+ !value ||
24
+ typeof value !== "object" ||
25
+ Array.isArray(value) ||
26
+ Object.keys(value).length !== keys.size ||
27
+ Object.keys(value).some((key) => !keys.has(key))
28
+ ) {
29
+ throw new Error(`${label} fields are invalid`);
30
+ }
31
+ }
32
+
33
+ function matchesDiscard(entry, snapshot) {
34
+ return (
35
+ snapshot &&
36
+ (entry.anchorDigest === snapshot.anchorDigest ||
37
+ entry.headDigest === snapshot.headDigest ||
38
+ entry.segmentDigest === snapshot.segmentDigest)
39
+ );
40
+ }
41
+
42
+ /** Internal storage adapter. Record signatures and trust policy stay with the witness. */
43
+ export function createEvolutionWitnessSegments({
44
+ filePath,
45
+ fsImpl,
46
+ readBytes,
47
+ writeBytes,
48
+ confirmDurable,
49
+ validateStore,
50
+ legacySchema,
51
+ canonical,
52
+ hash,
53
+ encodeRecord,
54
+ maximumHistoryBytes,
55
+ }) {
56
+ const segmentDirectory = `${filePath}.segments-v2`;
57
+ const secureOptions = { deps: { fs: fsImpl }, failIfUnavailable: true };
58
+ const metadata = new WeakMap();
59
+ const cache = new Map();
60
+ let cacheBytes = 0;
61
+
62
+ const segmentPath = (digest) => {
63
+ if (!DIGEST.test(digest || ""))
64
+ throw new Error("witness segment digest is invalid");
65
+ return path.join(segmentDirectory, `${digest.slice(7)}.json`);
66
+ };
67
+
68
+ const assertDirectory = () => {
69
+ ensurePrivateDirectory(segmentDirectory, secureOptions);
70
+ const observed = fsImpl.lstatSync(segmentDirectory);
71
+ if (!observed.isDirectory() || observed.isSymbolicLink()) {
72
+ throw new Error("witness segment directory is invalid");
73
+ }
74
+ };
75
+
76
+ const validateSegment = (segment, previous, trustEpoch) => {
77
+ exact(segment, SEGMENT_KEYS, "witness segment");
78
+ if (
79
+ segment.schema !== SEGMENT_SCHEMA ||
80
+ !Array.isArray(segment.history) ||
81
+ segment.history.length !== EVOLUTION_WITNESS_SEGMENT_RECORDS
82
+ ) {
83
+ throw new Error("witness segment length or schema is invalid");
84
+ }
85
+ validateStore(
86
+ {
87
+ schema: legacySchema,
88
+ current: segment.history.at(-1),
89
+ history: segment.history,
90
+ discardedAnchors: segment.discardedAnchors,
91
+ },
92
+ trustEpoch,
93
+ previous,
94
+ );
95
+ return segment;
96
+ };
97
+
98
+ const observe = (records, discardedAnchors, observations) => {
99
+ for (const query of observations.discards) {
100
+ if (
101
+ discardedAnchors.some((entry) => matchesDiscard(entry, query.snapshot))
102
+ ) {
103
+ query.matched = true;
104
+ }
105
+ }
106
+ for (const query of observations.records) {
107
+ const firstGeneration = records[0].generation;
108
+ const index = query.checkpoint.generation - firstGeneration;
109
+ if (Number.isSafeInteger(index) && index >= 0 && index < records.length) {
110
+ const record = records[index];
111
+ if (record.witnessDigest === query.checkpoint.witnessDigest)
112
+ query.record = record;
113
+ }
114
+ }
115
+ };
116
+
117
+ const remember = (digest, segment, trustEpoch) => {
118
+ if (trustEpoch === null) return;
119
+ // A cached decision belongs only to bytes rehashed on this read and a
120
+ // current verifier epoch. Copy through a small encoding so no parser
121
+ // substring can retain a whole source segment.
122
+ const encoded = canonical({
123
+ first: segment.history[0],
124
+ last: segment.history.at(-1),
125
+ discardedAnchors: segment.discardedAnchors,
126
+ });
127
+ const bytes = 6 * encoded.length;
128
+ const existing = cache.get(digest);
129
+ if (existing) cacheBytes -= existing.bytes;
130
+ cache.delete(digest);
131
+ if (cache.size >= MAX_SEGMENTS || cacheBytes + bytes > MAX_CACHE_BYTES)
132
+ return;
133
+ cache.set(digest, { ...JSON.parse(encoded), trustEpoch, bytes });
134
+ cacheBytes += bytes;
135
+ };
136
+
137
+ const load = (head, trustEpoch, options = {}) => {
138
+ exact(head, HEAD_KEYS, "segmented witness store");
139
+ if (
140
+ head.schema !== EVOLUTION_SEGMENTED_WITNESS_SCHEMA ||
141
+ !Array.isArray(head.segments) ||
142
+ head.segments.length < 1 ||
143
+ head.segments.length >= MAX_SEGMENTS ||
144
+ !Array.isArray(head.history) ||
145
+ head.history.length < 1 ||
146
+ head.history.length > EVOLUTION_WITNESS_SEGMENT_RECORDS ||
147
+ head.segments.length * EVOLUTION_WITNESS_SEGMENT_RECORDS +
148
+ head.history.length >
149
+ MAX_RECORDS
150
+ ) {
151
+ throw new Error("segmented witness store capacity or schema is invalid");
152
+ }
153
+ assertDirectory();
154
+ const seen = new Set();
155
+ const observations = {
156
+ discards: (options.discards || [])
157
+ .filter(Boolean)
158
+ .map((snapshot) => ({ snapshot, matched: false })),
159
+ records: (options.records || []).map((checkpoint) => ({
160
+ checkpoint,
161
+ record: null,
162
+ })),
163
+ };
164
+ let previous = null;
165
+ let storedBytes = options.headBytes;
166
+ if (
167
+ !Number.isSafeInteger(storedBytes) ||
168
+ storedBytes < 1 ||
169
+ storedBytes > maximumHistoryBytes
170
+ ) {
171
+ throw new Error("witness history exceeds its configured maximum size");
172
+ }
173
+ for (const digest of head.segments) {
174
+ if (seen.has(digest)) throw new Error("witness segment is repeated");
175
+ seen.add(digest);
176
+ const bytes = readBytes(
177
+ segmentPath(digest),
178
+ maximumHistoryBytes - storedBytes,
179
+ );
180
+ storedBytes += bytes.length;
181
+ if (hash(bytes) !== digest)
182
+ throw new Error("witness segment bytes changed");
183
+ const cached = cache.get(digest);
184
+ const canReuse =
185
+ cached && trustEpoch !== null && cached.trustEpoch === trustEpoch;
186
+ const needsRecord = observations.records.some(
187
+ ({ checkpoint }) =>
188
+ canReuse &&
189
+ checkpoint.generation >= cached.first.generation &&
190
+ checkpoint.generation <= cached.last.generation,
191
+ );
192
+ if (canReuse) {
193
+ if (
194
+ (!previous && cached.first.generation !== 0) ||
195
+ (previous &&
196
+ (cached.first.generation !== previous.generation + 1 ||
197
+ cached.first.previousWitnessDigest !== previous.witnessDigest))
198
+ ) {
199
+ throw new Error("witness segment history is not contiguous");
200
+ }
201
+ for (const query of observations.discards) {
202
+ if (
203
+ cached.discardedAnchors.some((entry) =>
204
+ matchesDiscard(entry, query.snapshot),
205
+ )
206
+ ) {
207
+ query.matched = true;
208
+ }
209
+ }
210
+ if (needsRecord) {
211
+ const parsed = JSON.parse(
212
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
213
+ );
214
+ observe(parsed.history, parsed.discardedAnchors, observations);
215
+ }
216
+ previous = cached.last;
217
+ continue;
218
+ }
219
+ const parsed = JSON.parse(
220
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
221
+ );
222
+ const segment = validateSegment(parsed, previous, trustEpoch);
223
+ observe(segment.history, segment.discardedAnchors, observations);
224
+ remember(digest, segment, trustEpoch);
225
+ previous = segment.history.at(-1);
226
+ }
227
+ const store = validateStore(
228
+ {
229
+ schema: legacySchema,
230
+ current: head.current,
231
+ history: head.history,
232
+ discardedAnchors: head.discardedAnchors,
233
+ },
234
+ trustEpoch,
235
+ previous,
236
+ );
237
+ observe(store.history, store.discardedAnchors, observations);
238
+ metadata.set(store, {
239
+ segments: [...head.segments],
240
+ observations,
241
+ tailPreviousDiscardDigest: previous.discardAccumulatorDigest,
242
+ segmentBytes: storedBytes - options.headBytes,
243
+ });
244
+ return store;
245
+ };
246
+
247
+ const publish = (store) => {
248
+ const existing = metadata.get(store);
249
+ const segments = [...(existing?.segments || [])];
250
+ if (
251
+ segments.length * EVOLUTION_WITNESS_SEGMENT_RECORDS +
252
+ store.history.length >
253
+ MAX_RECORDS
254
+ ) {
255
+ throw new Error("witness history exceeds its configured record capacity");
256
+ }
257
+ assertDirectory();
258
+ let history = store.history;
259
+ let discardedAnchors = store.discardedAnchors;
260
+ let segmentBytes = existing?.segmentBytes ?? 0;
261
+ let accumulator =
262
+ existing?.tailPreviousDiscardDigest ??
263
+ history[0].discardAccumulatorDigest;
264
+ while (history.length > EVOLUTION_WITNESS_SEGMENT_RECORDS) {
265
+ const records = history.slice(0, EVOLUTION_WITNESS_SEGMENT_RECORDS);
266
+ let discardCount = 0;
267
+ // The first record's predecessor may belong to the preceding segment.
268
+ // The caller carries the exact predecessor accumulator when loading.
269
+ for (const record of records) {
270
+ if (record.discardAccumulatorDigest !== accumulator) discardCount++;
271
+ accumulator = record.discardAccumulatorDigest;
272
+ }
273
+ const part = {
274
+ schema: SEGMENT_SCHEMA,
275
+ history: records,
276
+ discardedAnchors: discardedAnchors.slice(0, discardCount),
277
+ };
278
+ const bytes = Buffer.from(`${canonical(part)}\n`, "utf8");
279
+ segmentBytes += bytes.length;
280
+ if (segmentBytes > maximumHistoryBytes) {
281
+ throw new Error("witness history exceeds its configured maximum size");
282
+ }
283
+ const digest = hash(bytes);
284
+ const destination = segmentPath(digest);
285
+ if (fsImpl.existsSync(destination)) {
286
+ if (!readBytes(destination).equals(bytes)) {
287
+ throw new Error(
288
+ "existing witness segment differs from its content address",
289
+ );
290
+ }
291
+ // A previous process may have stopped after rename and before fsync of
292
+ // the directory. Reuse requires that durability boundary again.
293
+ confirmDurable(destination, bytes);
294
+ } else {
295
+ // The witness CAS lock serializes publication. Only complete, fsynced
296
+ // bytes are renamed into this immutable content address before HEAD.
297
+ writeBytes(destination, bytes);
298
+ }
299
+ segments.push(digest);
300
+ history = history.slice(EVOLUTION_WITNESS_SEGMENT_RECORDS);
301
+ discardedAnchors = discardedAnchors.slice(discardCount);
302
+ }
303
+ const encoded = `{"current":${encodeRecord(store.current)},"discardedAnchors":${canonical(discardedAnchors)},"history":[${history.map(encodeRecord).join(",")}],"schema":${JSON.stringify(EVOLUTION_SEGMENTED_WITNESS_SCHEMA)},"segments":${canonical(segments)}}`;
304
+ const headBytes = Buffer.from(`${encoded}\n`, "utf8");
305
+ if (segmentBytes + headBytes.length > maximumHistoryBytes) {
306
+ throw new Error("witness history exceeds its configured maximum size");
307
+ }
308
+ writeBytes(filePath, headBytes);
309
+ return store.current;
310
+ };
311
+
312
+ return {
313
+ load,
314
+ publish,
315
+ handles: (store) =>
316
+ metadata.has(store) ||
317
+ store.history.length > EVOLUTION_WITNESS_SEGMENT_RECORDS,
318
+ isSegmented: (store) => metadata.has(store),
319
+ isDiscarded: (store, snapshot) =>
320
+ metadata
321
+ .get(store)
322
+ ?.observations.discards.find((query) => query.snapshot === snapshot)
323
+ ?.matched === true,
324
+ lookup: (store, checkpoint) =>
325
+ metadata
326
+ .get(store)
327
+ ?.observations.records.find((query) => query.checkpoint === checkpoint)
328
+ ?.record ?? null,
329
+ };
330
+ }
@@ -218,6 +218,7 @@ export function createProgressiveCanaryHeartbeatAuthority({
218
218
  const authority = Object.freeze({
219
219
  planDigest: plan.planDigest,
220
220
  async issue({ sequence, stage, activeStateDigest } = {}) {
221
+ const issuedAt = timestamp(Number(now()), "heartbeat clock");
221
222
  const payload = {
222
223
  schema: PROGRESSIVE_CANARY_HOST_HEARTBEAT_SCHEMA,
223
224
  planDigest: plan.planDigest,
@@ -228,15 +229,16 @@ export function createProgressiveCanaryHeartbeatAuthority({
228
229
  sequence: timestamp(sequence, "heartbeat sequence"),
229
230
  stage: id(stage, "heartbeat stage"),
230
231
  activeStateDigest: digest(activeStateDigest, "activeStateDigest"),
231
- issuedAt: timestamp(Number(now()), "heartbeat clock"),
232
- expiresAt: Number(now()) + plan.leaseDurationMs,
232
+ issuedAt,
233
+ expiresAt: timestamp(
234
+ issuedAt + plan.leaseDurationMs,
235
+ "heartbeat expiry",
236
+ ),
233
237
  authorityId: plan.heartbeatAuthority.id,
234
238
  authorityRevision: plan.heartbeatAuthority.revision,
235
239
  handlerDigest: plan.heartbeatAuthority.handlerDigest,
236
240
  publicKeySpkiDigest: plan.heartbeatAuthority.publicKeySpkiDigest,
237
241
  };
238
- if (payload.expiresAt !== payload.issuedAt + plan.leaseDurationMs)
239
- throw new Error("heartbeat clock changed during issuance");
240
242
  const signature = await attestor(Object.freeze(structuredClone(payload)));
241
243
  const core = { ...payload, signature };
242
244
  return verify(
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { types as utilTypes } from "node:util";
5
5
  import { getHomeDir } from "../paths.js";
6
6
  import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
7
+ import { withEvolutionFileIdentity } from "./evolution-file-identity.js";
7
8
  import {
8
9
  verifySkillDependencyLock,
9
10
  verifySkillRuntimeManifest,
@@ -1793,6 +1794,15 @@ export class SkillCandidateRegistry {
1793
1794
  }
1794
1795
 
1795
1796
  _initializeTenantMarker() {
1797
+ return withEvolutionFileIdentity(
1798
+ this._fs,
1799
+ this._markerPath,
1800
+ (_full, sameStable) =>
1801
+ this.#initializeTenantMarkerWithIdentity(sameStable),
1802
+ );
1803
+ }
1804
+
1805
+ #initializeTenantMarkerWithIdentity(sameStable) {
1796
1806
  const existing = lstatOrNull(this._fs, this._markerPath);
1797
1807
  if (existing) {
1798
1808
  const verified = this._readAndVerifyTenantMarker();
@@ -1832,7 +1842,6 @@ export class SkillCandidateRegistry {
1832
1842
  "candidate tenant marker temporary file is unsafe",
1833
1843
  );
1834
1844
  }
1835
- const writtenIdentity = entryIdentity(written);
1836
1845
  this._fs.closeSync(descriptor);
1837
1846
  descriptor = null;
1838
1847
  if (this._secure) {
@@ -1847,7 +1856,7 @@ export class SkillCandidateRegistry {
1847
1856
  staged.isSymbolicLink() ||
1848
1857
  Number(staged.nlink) !== 1 ||
1849
1858
  staged.size !== bytes.length ||
1850
- entryIdentity(staged) !== writtenIdentity ||
1859
+ !sameStable(staged, written) ||
1851
1860
  !samePath(realpath(this._fs, temporaryPath), temporaryPath)
1852
1861
  ) {
1853
1862
  throw registryError(
@@ -1870,7 +1879,7 @@ export class SkillCandidateRegistry {
1870
1879
  !linked.isFile() ||
1871
1880
  linked.isSymbolicLink() ||
1872
1881
  Number(linked.nlink) !== 2 ||
1873
- entryIdentity(linked) !== writtenIdentity
1882
+ entryIdentity(linked) !== entryIdentity(staged)
1874
1883
  ) {
1875
1884
  throw registryError(
1876
1885
  "SKILL_CANDIDATE_STORE_UNSAFE",
@@ -1901,6 +1910,24 @@ export class SkillCandidateRegistry {
1901
1910
  }
1902
1911
 
1903
1912
  _readBoundedRegularFile(filePath, maximum, code, label) {
1913
+ return withEvolutionFileIdentity(this._fs, filePath, (samePathHandle) =>
1914
+ this.#readBoundedRegularFileWithIdentity(
1915
+ filePath,
1916
+ maximum,
1917
+ code,
1918
+ label,
1919
+ samePathHandle,
1920
+ ),
1921
+ );
1922
+ }
1923
+
1924
+ #readBoundedRegularFileWithIdentity(
1925
+ filePath,
1926
+ maximum,
1927
+ code,
1928
+ label,
1929
+ samePathHandle,
1930
+ ) {
1904
1931
  let descriptor = null;
1905
1932
  try {
1906
1933
  const before = this._fs.lstatSync(filePath);
@@ -1925,7 +1952,7 @@ export class SkillCandidateRegistry {
1925
1952
  if (
1926
1953
  !opened.isFile() ||
1927
1954
  Number(opened.nlink) !== 1 ||
1928
- entryIdentity(opened) !== entryIdentity(before) ||
1955
+ !samePathHandle(before, opened) ||
1929
1956
  opened.size !== before.size
1930
1957
  ) {
1931
1958
  throw registryError(code, `${label} changed while it was opened`);
@@ -1941,12 +1968,12 @@ export class SkillCandidateRegistry {
1941
1968
  !afterPath.isFile() ||
1942
1969
  afterPath.isSymbolicLink() ||
1943
1970
  Number(afterPath.nlink) !== 1 ||
1944
- entryIdentity(afterPath) !== entryIdentity(opened) ||
1971
+ !samePathHandle(afterPath, after) ||
1945
1972
  !samePath(realpath(this._fs, filePath), filePath)
1946
1973
  ) {
1947
1974
  throw registryError(code, `${label} changed while it was read`);
1948
1975
  }
1949
- return { bytes, identity: entryIdentity(opened) };
1976
+ return { bytes, identity: entryIdentity(before) };
1950
1977
  } finally {
1951
1978
  if (descriptor !== null) this._fs.closeSync(descriptor);
1952
1979
  }
@@ -2637,6 +2664,14 @@ export class SkillCandidateRegistry {
2637
2664
  "candidate create accepts only input; admission context is registry-owned",
2638
2665
  );
2639
2666
  }
2667
+ return withEvolutionFileIdentity(
2668
+ this._fs,
2669
+ this._markerPath,
2670
+ (_full, sameStable) => this.#createWithIdentity(input, sameStable),
2671
+ );
2672
+ }
2673
+
2674
+ #createWithIdentity(input, sameStable) {
2640
2675
  this._assertNoMixedTenantArtifacts();
2641
2676
  const verificationContext = this._resolveAdmissionContext(input);
2642
2677
  const candidate = buildSkillCandidateDraft(input, verificationContext);
@@ -2684,7 +2719,6 @@ export class SkillCandidateRegistry {
2684
2719
  { candidateId: candidate.candidateId, commitState: "not-committed" },
2685
2720
  );
2686
2721
  }
2687
- const writtenIdentity = entryIdentity(written);
2688
2722
  this._fs.closeSync(descriptor);
2689
2723
  descriptor = null;
2690
2724
  if (this._secure) {
@@ -2699,7 +2733,7 @@ export class SkillCandidateRegistry {
2699
2733
  staged.isSymbolicLink() ||
2700
2734
  Number(staged.nlink) !== 1 ||
2701
2735
  staged.size !== bytes.length ||
2702
- entryIdentity(staged) !== writtenIdentity ||
2736
+ !sameStable(staged, written) ||
2703
2737
  !samePath(realpath(this._fs, temporaryPath), temporaryPath)
2704
2738
  ) {
2705
2739
  throw registryError(
@@ -2742,7 +2776,7 @@ export class SkillCandidateRegistry {
2742
2776
  linked.isSymbolicLink() ||
2743
2777
  Number(linked.nlink) !== 2 ||
2744
2778
  linked.size !== bytes.length ||
2745
- entryIdentity(linked) !== writtenIdentity ||
2779
+ entryIdentity(linked) !== entryIdentity(staged) ||
2746
2780
  !samePath(realpath(this._fs, filePath), filePath)
2747
2781
  ) {
2748
2782
  throw registryError(
@@ -2759,7 +2793,7 @@ export class SkillCandidateRegistry {
2759
2793
  finalized.isSymbolicLink() ||
2760
2794
  Number(finalized.nlink) !== 1 ||
2761
2795
  finalized.size !== bytes.length ||
2762
- entryIdentity(finalized) !== writtenIdentity
2796
+ entryIdentity(finalized) !== entryIdentity(staged)
2763
2797
  ) {
2764
2798
  throw registryError(
2765
2799
  "SKILL_CANDIDATE_COMMIT_UNKNOWN",
@@ -2794,7 +2828,7 @@ export class SkillCandidateRegistry {
2794
2828
  );
2795
2829
  }
2796
2830
  if (
2797
- stored.identity !== writtenIdentity ||
2831
+ stored.identity !== entryIdentity(staged) ||
2798
2832
  verified.tenantId !== this.tenantId ||
2799
2833
  verified.candidateId !== candidate.candidateId ||
2800
2834
  !stored.bytes.equals(bytes) ||
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { types as utilTypes } from "node:util";
6
6
  import { getHomeDir } from "../paths.js";
7
7
  import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
8
+ import { withEvolutionFileIdentity } from "./evolution-file-identity.js";
8
9
  import {
9
10
  SKILL_CANDIDATE_MIGRATION_RECEIPT_SCHEMA,
10
11
  SKILL_CANDIDATE_MIGRATION_RECORD_SCHEMA,
@@ -2543,6 +2544,26 @@ function lstatOrNull(fsImpl, target) {
2543
2544
  }
2544
2545
 
2545
2546
  function readBoundedSingleLinkFile(fsImpl, filePath, maximum, code, label) {
2547
+ return withEvolutionFileIdentity(fsImpl, filePath, (samePathHandle) =>
2548
+ readBoundedSingleLinkFileWithIdentity(
2549
+ fsImpl,
2550
+ filePath,
2551
+ maximum,
2552
+ code,
2553
+ label,
2554
+ samePathHandle,
2555
+ ),
2556
+ );
2557
+ }
2558
+
2559
+ function readBoundedSingleLinkFileWithIdentity(
2560
+ fsImpl,
2561
+ filePath,
2562
+ maximum,
2563
+ code,
2564
+ label,
2565
+ samePathHandle,
2566
+ ) {
2546
2567
  let descriptor = null;
2547
2568
  try {
2548
2569
  const before = fsImpl.lstatSync(filePath);
@@ -2564,7 +2585,7 @@ function readBoundedSingleLinkFile(fsImpl, filePath, maximum, code, label) {
2564
2585
  if (
2565
2586
  !opened.isFile() ||
2566
2587
  Number(opened.nlink) !== 1 ||
2567
- identity(opened) !== identity(before) ||
2588
+ !samePathHandle(before, opened) ||
2568
2589
  opened.size !== before.size
2569
2590
  ) {
2570
2591
  throw failure(code, `${label} changed while opening`);
@@ -2580,12 +2601,12 @@ function readBoundedSingleLinkFile(fsImpl, filePath, maximum, code, label) {
2580
2601
  !afterPath.isFile() ||
2581
2602
  afterPath.isSymbolicLink() ||
2582
2603
  Number(afterPath.nlink) !== 1 ||
2583
- identity(afterPath) !== identity(opened) ||
2604
+ !samePathHandle(afterPath, after) ||
2584
2605
  !samePath(realpath(fsImpl, filePath), filePath)
2585
2606
  ) {
2586
2607
  throw failure(code, `${label} changed while reading`);
2587
2608
  }
2588
- return { bytes, identity: identity(opened) };
2609
+ return { bytes, identity: identity(before) };
2589
2610
  } finally {
2590
2611
  if (descriptor !== null) fsImpl.closeSync(descriptor);
2591
2612
  }
@@ -2786,8 +2807,7 @@ export class SkillReleaseRegistry {
2786
2807
  readActive: SkillReleaseRegistry.prototype.readActive.bind(this),
2787
2808
  readState: SkillReleaseRegistry.prototype.readState.bind(this),
2788
2809
  readRelease: SkillReleaseRegistry.prototype.readRelease.bind(this),
2789
- readInventory:
2790
- SkillReleaseRegistry.prototype.readInventory.bind(this),
2810
+ readInventory: SkillReleaseRegistry.prototype.readInventory.bind(this),
2791
2811
  matchesTransactionLedger: (value) => value === transactionLedger,
2792
2812
  }),
2793
2813
  );
@@ -2870,6 +2890,15 @@ export class SkillReleaseRegistry {
2870
2890
  }
2871
2891
 
2872
2892
  #initializeTenantMarker() {
2893
+ return withEvolutionFileIdentity(
2894
+ this.#fs,
2895
+ this.#markerPath,
2896
+ (_full, sameStable) =>
2897
+ this.#initializeTenantMarkerWithIdentity(sameStable),
2898
+ );
2899
+ }
2900
+
2901
+ #initializeTenantMarkerWithIdentity(sameStable) {
2873
2902
  const existing = lstatOrNull(this.#fs, this.#markerPath);
2874
2903
  if (existing) {
2875
2904
  const verified = this.#readAndVerifyTenantMarker();
@@ -2905,7 +2934,6 @@ export class SkillReleaseRegistry {
2905
2934
  "release tenant marker temporary file is unsafe",
2906
2935
  );
2907
2936
  }
2908
- const writtenIdentity = identity(written);
2909
2937
  this.#fs.closeSync(descriptor);
2910
2938
  descriptor = null;
2911
2939
  if (this.#secure) {
@@ -2920,7 +2948,7 @@ export class SkillReleaseRegistry {
2920
2948
  staged.isSymbolicLink() ||
2921
2949
  Number(staged.nlink) !== 1 ||
2922
2950
  staged.size !== bytes.length ||
2923
- identity(staged) !== writtenIdentity ||
2951
+ !sameStable(staged, written) ||
2924
2952
  !samePath(realpath(this.#fs, temporaryPath), temporaryPath)
2925
2953
  ) {
2926
2954
  throw failure(
@@ -2943,7 +2971,7 @@ export class SkillReleaseRegistry {
2943
2971
  !linked.isFile() ||
2944
2972
  linked.isSymbolicLink() ||
2945
2973
  Number(linked.nlink) !== 2 ||
2946
- identity(linked) !== writtenIdentity
2974
+ identity(linked) !== identity(staged)
2947
2975
  ) {
2948
2976
  throw failure(
2949
2977
  "SKILL_RELEASE_STORE_UNSAFE",
@@ -3229,6 +3257,12 @@ export class SkillReleaseRegistry {
3229
3257
  throw failure("SKILL_RELEASE_STORE_UNSAFE", "temporary name is invalid");
3230
3258
  }
3231
3259
  const filePath = this.#path("staging", fileName);
3260
+ return withEvolutionFileIdentity(this.#fs, filePath, (_full, sameStable) =>
3261
+ this.#writeTemporaryWithIdentity(filePath, value, sameStable),
3262
+ );
3263
+ }
3264
+
3265
+ #writeTemporaryWithIdentity(filePath, value, sameStable) {
3232
3266
  const bytes = serialize(value);
3233
3267
  let descriptor = null;
3234
3268
  let writtenIdentity = null;
@@ -3248,7 +3282,7 @@ export class SkillReleaseRegistry {
3248
3282
  "temporary write was incomplete",
3249
3283
  );
3250
3284
  }
3251
- writtenIdentity = identity(written);
3285
+ writtenIdentity = written;
3252
3286
  } finally {
3253
3287
  if (descriptor !== null) this.#fs.closeSync(descriptor);
3254
3288
  }
@@ -3263,7 +3297,7 @@ export class SkillReleaseRegistry {
3263
3297
  !staged.isFile() ||
3264
3298
  staged.isSymbolicLink() ||
3265
3299
  Number(staged.nlink) !== 1 ||
3266
- identity(staged) !== writtenIdentity ||
3300
+ !sameStable(staged, writtenIdentity) ||
3267
3301
  staged.size !== bytes.length ||
3268
3302
  !samePath(realpath(this.#fs, filePath), filePath)
3269
3303
  ) {
@@ -4006,8 +4040,8 @@ export class SkillReleaseRegistry {
4006
4040
  }
4007
4041
  const afterReleases = this.#readInventoryNames("artifacts", releasePattern);
4008
4042
  const afterStates = this.#readInventoryNames("active", statePattern);
4009
- const afterStateDigests = afterStates.map((name) =>
4010
- this.readState(name.slice(0, -".json".length)).stateDigest,
4043
+ const afterStateDigests = afterStates.map(
4044
+ (name) => this.readState(name.slice(0, -".json".length)).stateDigest,
4011
4045
  );
4012
4046
  if (
4013
4047
  canonicalJson(beforeReleases) !== canonicalJson(afterReleases) ||