@narumitw/pi-sync 0.57.9 → 0.57.11

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/CHANGELOG.md +14 -0
  2. package/README.md +7 -1
  3. package/dist/chunks/{agent-candidate-GNEYR225.ts → agent-candidate-OCDPRI5G.ts} +12 -9
  4. package/dist/chunks/{agent-plan-3L7CRFDM.ts → agent-plan-P5B5RGZ7.ts} +22 -18
  5. package/dist/chunks/{agent-plan-3L7CRFDM.ts.map → agent-plan-P5B5RGZ7.ts.map} +1 -1
  6. package/dist/chunks/{agent-refusal-retry-RPTMUQXK.ts → agent-refusal-retry-476YGNU5.ts} +11 -8
  7. package/dist/chunks/{agent-refusal-retry-RPTMUQXK.ts.map → agent-refusal-retry-476YGNU5.ts.map} +1 -1
  8. package/dist/chunks/{agent-store-55CZOCPX.ts → agent-store-RJSMNOYP.ts} +12 -11
  9. package/dist/chunks/{agent-store-55CZOCPX.ts.map → agent-store-RJSMNOYP.ts.map} +1 -1
  10. package/dist/chunks/agent-sync-EUK2CPSS.ts +22 -0
  11. package/dist/chunks/{chunk-U3XRI2HF.ts → chunk-3IYAPMST.ts} +11 -11
  12. package/dist/chunks/{chunk-B4QZBDBT.ts → chunk-45COPAK7.ts} +10 -3
  13. package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
  14. package/dist/chunks/chunk-5SFMBY5P.ts +26 -0
  15. package/dist/chunks/chunk-5SFMBY5P.ts.map +7 -0
  16. package/dist/chunks/{chunk-ED3TYIVW.ts → chunk-5TSCBBPP.ts} +5 -5
  17. package/dist/chunks/{chunk-5QAE3G3K.ts → chunk-67INJPKR.ts} +6 -6
  18. package/dist/chunks/{chunk-IWZH5KYS.ts → chunk-6LD5KX55.ts} +10 -10
  19. package/dist/chunks/{chunk-AAEEHR7H.ts → chunk-6O3EDBRL.ts} +2 -2
  20. package/dist/chunks/{chunk-CRKTAPSL.ts → chunk-7A35ZAVO.ts} +333 -237
  21. package/dist/chunks/chunk-7A35ZAVO.ts.map +7 -0
  22. package/dist/chunks/{chunk-6FVVRTL5.ts → chunk-7OLLPPLU.ts} +77 -63
  23. package/dist/chunks/chunk-7OLLPPLU.ts.map +7 -0
  24. package/dist/chunks/{chunk-PQW6GS4J.ts → chunk-7ZIYU5QS.ts} +12 -10
  25. package/dist/chunks/{chunk-PQW6GS4J.ts.map → chunk-7ZIYU5QS.ts.map} +1 -1
  26. package/dist/chunks/{chunk-S7QS4AWK.ts → chunk-AKMV47YC.ts} +5 -5
  27. package/dist/chunks/chunk-AR2DGDUI.ts +55 -0
  28. package/dist/chunks/chunk-AR2DGDUI.ts.map +7 -0
  29. package/dist/chunks/{chunk-GNH73BJ5.ts → chunk-BEST45H2.ts} +7 -7
  30. package/dist/chunks/chunk-CBLTWXAU.ts +114 -0
  31. package/dist/chunks/chunk-CBLTWXAU.ts.map +7 -0
  32. package/dist/chunks/{chunk-GP5DU62N.ts → chunk-CPT2BN6N.ts} +2 -2
  33. package/dist/chunks/{chunk-UPNVIUBN.ts → chunk-DN4IA7MR.ts} +6 -6
  34. package/dist/chunks/{chunk-PG7NMFP2.ts → chunk-DRTC4PKM.ts} +8 -8
  35. package/dist/chunks/chunk-ESKOPQVB.ts +348 -0
  36. package/dist/chunks/chunk-ESKOPQVB.ts.map +7 -0
  37. package/dist/chunks/{chunk-MKKF4IMY.ts → chunk-F7E3VFXU.ts} +5 -5
  38. package/dist/chunks/{chunk-HUZ5CR4W.ts → chunk-FHE2DCA2.ts} +3 -52
  39. package/dist/chunks/chunk-FHE2DCA2.ts.map +7 -0
  40. package/dist/chunks/{chunk-UBV24CG4.ts → chunk-FLHCOJCD.ts} +3 -3
  41. package/dist/chunks/chunk-GNYGNHFF.ts +27 -0
  42. package/dist/chunks/chunk-GNYGNHFF.ts.map +7 -0
  43. package/dist/chunks/chunk-KYTT7KV4.ts +319 -0
  44. package/dist/chunks/chunk-KYTT7KV4.ts.map +7 -0
  45. package/dist/chunks/{chunk-A6HU4ERM.ts → chunk-MKSMJPVA.ts} +6 -23
  46. package/dist/chunks/chunk-MKSMJPVA.ts.map +7 -0
  47. package/dist/chunks/{chunk-ORGBI36T.ts → chunk-OJARL2TG.ts} +4 -4
  48. package/dist/chunks/{chunk-JCH3VXIO.ts → chunk-OKPPZA4D.ts} +150 -91
  49. package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
  50. package/dist/chunks/{chunk-QO333NM4.ts → chunk-OVBAW7KV.ts} +180 -133
  51. package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
  52. package/dist/chunks/{chunk-GSNPASKB.ts → chunk-Q23JJW7E.ts} +6 -6
  53. package/dist/chunks/{chunk-YK52VDXG.ts → chunk-TIMWN2RH.ts} +10 -8
  54. package/dist/chunks/{chunk-YK52VDXG.ts.map → chunk-TIMWN2RH.ts.map} +1 -1
  55. package/dist/chunks/{chunk-UOHR7LRO.ts → chunk-TZC7GDJE.ts} +10 -22
  56. package/dist/chunks/chunk-TZC7GDJE.ts.map +7 -0
  57. package/dist/chunks/chunk-X7R6DCVF.ts +28 -0
  58. package/dist/chunks/chunk-X7R6DCVF.ts.map +7 -0
  59. package/dist/chunks/{chunk-76NB4R7U.ts → chunk-XJHQPB3E.ts} +2 -2
  60. package/dist/chunks/{chunk-BDB4IDM3.ts → chunk-Y4UVXOLQ.ts} +8 -6
  61. package/dist/chunks/{chunk-BDB4IDM3.ts.map → chunk-Y4UVXOLQ.ts.map} +1 -1
  62. package/dist/chunks/{chunk-P7DD5PWZ.ts → chunk-ZDDBZH72.ts} +3 -3
  63. package/dist/chunks/command-handler-F2E7A5T5.ts +548 -0
  64. package/dist/chunks/command-handler-F2E7A5T5.ts.map +7 -0
  65. package/dist/chunks/{config-KGIYTTXQ.ts → config-HYG7G5EM.ts} +7 -6
  66. package/dist/chunks/{conflict-artifacts-FXFIIBCB.ts → conflict-artifacts-J6WSDCWP.ts} +13 -9
  67. package/dist/chunks/{conflict-review-3X37QEIT.ts → conflict-review-2QMO2K6Q.ts} +31 -25
  68. package/dist/chunks/{conflict-review-3X37QEIT.ts.map → conflict-review-2QMO2K6Q.ts.map} +1 -1
  69. package/dist/chunks/{continuous-record-SQJ47CXN.ts → continuous-record-AH7JGYKM.ts} +8 -7
  70. package/dist/chunks/{continuous-sync-Q6QUD6TY.ts → continuous-sync-VRMXCA5X.ts} +33 -28
  71. package/dist/chunks/{continuous-sync-Q6QUD6TY.ts.map → continuous-sync-VRMXCA5X.ts.map} +1 -1
  72. package/dist/chunks/{file-selection-3JJYOVNR.ts → file-selection-IELTXSGZ.ts} +11 -10
  73. package/dist/chunks/{file-selection-3JJYOVNR.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
  74. package/dist/chunks/{git-backend-NYKSG5X6.ts → git-backend-KHD36W27.ts} +15 -12
  75. package/dist/chunks/{git-backend-NYKSG5X6.ts.map → git-backend-KHD36W27.ts.map} +1 -1
  76. package/dist/chunks/{local-fields-6DGYOOIK.ts → local-fields-JEKAPZKD.ts} +9 -5
  77. package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
  78. package/dist/chunks/{manager-ui-ZH6DEC2G.ts → manager-ui-LM7DSJ2Y.ts} +124 -67
  79. package/dist/chunks/manager-ui-LM7DSJ2Y.ts.map +7 -0
  80. package/dist/chunks/{merge-baseline-store-RFCAH7KL.ts → merge-baseline-store-65GKUMXI.ts} +6 -5
  81. package/dist/chunks/remote-selection-ui-TD4C4W3J.ts +31 -0
  82. package/dist/chunks/{s3-backend-F7PR5FB3.ts → s3-backend-L24UZ6IK.ts} +24 -18
  83. package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
  84. package/dist/chunks/{setup-switch-E7PHKI6J.ts → setup-switch-PIPXYPCW.ts} +6 -7
  85. package/dist/chunks/{setup-wizard-OVHZYXNW.ts → setup-wizard-RR5U3SUB.ts} +8 -9
  86. package/dist/chunks/{setup-wizard-OVHZYXNW.ts.map → setup-wizard-RR5U3SUB.ts.map} +1 -1
  87. package/dist/chunks/{snapshot-THS5EY2L.ts → snapshot-RGNDYT6A.ts} +10 -6
  88. package/dist/chunks/snapshot-transaction-26BQO36S.ts +24 -0
  89. package/dist/chunks/sync-inspection-MOMLGUTD.ts +25 -0
  90. package/dist/chunks/sync-inspection-MOMLGUTD.ts.map +7 -0
  91. package/dist/chunks/{sync-operations-Y72GNIQ2.ts → sync-operations-CV3TULLD.ts} +66 -57
  92. package/dist/chunks/{sync-operations-Y72GNIQ2.ts.map → sync-operations-CV3TULLD.ts.map} +1 -1
  93. package/dist/chunks/{sync-state-CFL4RRRB.ts → sync-state-7L4NT7W2.ts} +8 -5
  94. package/dist/chunks/sync-state-7L4NT7W2.ts.map +7 -0
  95. package/dist/chunks/{webdav-backend-TGFUH3SF.ts → webdav-backend-3ZCACILP.ts} +15 -12
  96. package/dist/chunks/{webdav-backend-TGFUH3SF.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
  97. package/dist/index.ts +54 -537
  98. package/dist/index.ts.map +3 -3
  99. package/package.json +2 -2
  100. package/skills/configure-pi-sync/references/configuration.md +8 -1
  101. package/src/backends/s3/s3-client.ts +8 -1
  102. package/src/commands/command-execution.ts +8 -3
  103. package/src/paths.ts +6 -1
  104. package/src/settings/config-file.ts +205 -118
  105. package/src/settings/config.ts +2 -2
  106. package/src/settings/settings-management.ts +15 -1
  107. package/src/settings/settings-store.ts +0 -14
  108. package/src/settings/settings-validation.ts +1 -1
  109. package/src/snapshot/session-paths.ts +15 -2
  110. package/src/snapshot/snapshot-mutation-queues.ts +142 -0
  111. package/src/snapshot/snapshot-transaction-directory.ts +16 -0
  112. package/src/snapshot/snapshot-transaction.ts +123 -137
  113. package/src/sync/automatic-sync.ts +12 -6
  114. package/src/sync/local-field-policy.ts +29 -0
  115. package/src/sync/local-fields.ts +3 -29
  116. package/src/sync/merge-apply.ts +6 -0
  117. package/src/sync/mutation-owner.ts +22 -0
  118. package/src/sync/startup-check.ts +3 -2
  119. package/src/sync/sync-loaders.ts +4 -0
  120. package/src/sync/sync-local.ts +2 -19
  121. package/src/sync-extension.ts +7 -2
  122. package/src/ui/cancellable-operation.ts +36 -13
  123. package/src/ui/manager-state.ts +3 -1
  124. package/src/ui/manager-ui.ts +76 -24
  125. package/src/ui/remote-selection-ui.ts +131 -82
  126. package/src/ui/settings-ui.ts +2 -1
  127. package/dist/chunks/chunk-6FVVRTL5.ts.map +0 -7
  128. package/dist/chunks/chunk-A6HU4ERM.ts.map +0 -7
  129. package/dist/chunks/chunk-B4QZBDBT.ts.map +0 -7
  130. package/dist/chunks/chunk-CRKTAPSL.ts.map +0 -7
  131. package/dist/chunks/chunk-HUZ5CR4W.ts.map +0 -7
  132. package/dist/chunks/chunk-JCH3VXIO.ts.map +0 -7
  133. package/dist/chunks/chunk-QO333NM4.ts.map +0 -7
  134. package/dist/chunks/chunk-QOFMXPM5.ts +0 -735
  135. package/dist/chunks/chunk-QOFMXPM5.ts.map +0 -7
  136. package/dist/chunks/chunk-UOHR7LRO.ts.map +0 -7
  137. package/dist/chunks/manager-result-dispatcher-HOCM2GDA.ts +0 -24
  138. package/dist/chunks/manager-ui-ZH6DEC2G.ts.map +0 -7
  139. package/dist/chunks/remote-selection-ui-YFPGYGNE.ts +0 -23
  140. package/dist/chunks/s3-backend-F7PR5FB3.ts.map +0 -7
  141. package/dist/chunks/sync-inspection-J25EEKZK.ts +0 -21
  142. /package/dist/chunks/{agent-candidate-GNEYR225.ts.map → agent-candidate-OCDPRI5G.ts.map} +0 -0
  143. /package/dist/chunks/{config-KGIYTTXQ.ts.map → agent-sync-EUK2CPSS.ts.map} +0 -0
  144. /package/dist/chunks/{chunk-U3XRI2HF.ts.map → chunk-3IYAPMST.ts.map} +0 -0
  145. /package/dist/chunks/{chunk-ED3TYIVW.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
  146. /package/dist/chunks/{chunk-5QAE3G3K.ts.map → chunk-67INJPKR.ts.map} +0 -0
  147. /package/dist/chunks/{chunk-IWZH5KYS.ts.map → chunk-6LD5KX55.ts.map} +0 -0
  148. /package/dist/chunks/{chunk-AAEEHR7H.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
  149. /package/dist/chunks/{chunk-S7QS4AWK.ts.map → chunk-AKMV47YC.ts.map} +0 -0
  150. /package/dist/chunks/{chunk-GNH73BJ5.ts.map → chunk-BEST45H2.ts.map} +0 -0
  151. /package/dist/chunks/{chunk-GP5DU62N.ts.map → chunk-CPT2BN6N.ts.map} +0 -0
  152. /package/dist/chunks/{chunk-UPNVIUBN.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
  153. /package/dist/chunks/{chunk-PG7NMFP2.ts.map → chunk-DRTC4PKM.ts.map} +0 -0
  154. /package/dist/chunks/{chunk-MKKF4IMY.ts.map → chunk-F7E3VFXU.ts.map} +0 -0
  155. /package/dist/chunks/{chunk-UBV24CG4.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
  156. /package/dist/chunks/{chunk-ORGBI36T.ts.map → chunk-OJARL2TG.ts.map} +0 -0
  157. /package/dist/chunks/{chunk-GSNPASKB.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
  158. /package/dist/chunks/{chunk-76NB4R7U.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
  159. /package/dist/chunks/{chunk-P7DD5PWZ.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
  160. /package/dist/chunks/{conflict-artifacts-FXFIIBCB.ts.map → config-HYG7G5EM.ts.map} +0 -0
  161. /package/dist/chunks/{continuous-record-SQJ47CXN.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
  162. /package/dist/chunks/{local-fields-6DGYOOIK.ts.map → continuous-record-AH7JGYKM.ts.map} +0 -0
  163. /package/dist/chunks/{manager-result-dispatcher-HOCM2GDA.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
  164. /package/dist/chunks/{merge-baseline-store-RFCAH7KL.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
  165. /package/dist/chunks/{remote-selection-ui-YFPGYGNE.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
  166. /package/dist/chunks/{setup-switch-E7PHKI6J.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
  167. /package/dist/chunks/{snapshot-THS5EY2L.ts.map → setup-switch-PIPXYPCW.ts.map} +0 -0
  168. /package/dist/chunks/{sync-inspection-J25EEKZK.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
  169. /package/dist/chunks/{sync-state-CFL4RRRB.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
@@ -1,27 +1,38 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
- withLock
5
- } from "./chunk-AAEEHR7H.ts";
4
+ pendingTransactionEntries,
5
+ transactionRoot
6
+ } from "./chunk-5SFMBY5P.ts";
6
7
  import {
7
- agentDir,
8
- configuredSessionDir,
9
- expandSessionDir,
10
- sessionStorageRoot
11
- } from "./chunk-QOFMXPM5.ts";
8
+ withLock
9
+ } from "./chunk-6O3EDBRL.ts";
12
10
  import {
13
11
  mergePathIdentity
14
- } from "./chunk-76NB4R7U.ts";
12
+ } from "./chunk-XJHQPB3E.ts";
13
+ import {
14
+ syncDirectory,
15
+ writeJson
16
+ } from "./chunk-AR2DGDUI.ts";
15
17
  import {
16
18
  stateDir
17
19
  } from "./chunk-SWRVXVFD.ts";
20
+ import {
21
+ agentDir,
22
+ configuredSessionDir,
23
+ expandSessionDir,
24
+ sessionStorageRoot
25
+ } from "./chunk-CBLTWXAU.ts";
18
26
  import {
19
27
  assertWithinRoot,
20
28
  isDeniedPath,
21
- isPathInside,
22
- syncDirectory,
23
- writeJson
24
- } from "./chunk-HUZ5CR4W.ts";
29
+ isPathInside
30
+ } from "./chunk-FHE2DCA2.ts";
31
+
32
+ // src/snapshot/snapshot-transaction.ts
33
+ import { createHash as createHash2, randomUUID } from "node:crypto";
34
+ import fs4 from "node:fs/promises";
35
+ import path6 from "node:path";
25
36
 
26
37
  // src/state/mutation-directory-sync.ts
27
38
  import path from "node:path";
@@ -50,12 +61,6 @@ async function syncMutationParents(targets, roots, options = {}) {
50
61
  }
51
62
  }
52
63
 
53
- // src/snapshot/snapshot-transaction.ts
54
- import { createHash as createHash2, randomUUID } from "node:crypto";
55
- import fs3 from "node:fs/promises";
56
- import path5 from "node:path";
57
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
58
-
59
64
  // src/snapshot/snapshot-case-replacement.ts
60
65
  import fs from "node:fs/promises";
61
66
  import path2 from "node:path";
@@ -126,13 +131,124 @@ async function coalesceCaseReplacements(root, plan) {
126
131
  };
127
132
  }
128
133
 
129
- // src/snapshot/snapshot-root-transition.ts
134
+ // src/snapshot/snapshot-mutation-queues.ts
130
135
  import fs2 from "node:fs/promises";
131
- import path4 from "node:path";
136
+ import path3 from "node:path";
137
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
138
+ function assertCurrent(options) {
139
+ options.validateMutation?.();
140
+ options.signal?.throwIfAborted();
141
+ }
142
+ function isMissing(error) {
143
+ const code = error.code;
144
+ return code === "ENOENT" || code === "ENOTDIR";
145
+ }
146
+ async function targetMissing(target, options) {
147
+ assertCurrent(options);
148
+ try {
149
+ await fs2.lstat(target);
150
+ assertCurrent(options);
151
+ return false;
152
+ } catch (error) {
153
+ assertCurrent(options);
154
+ if (!isMissing(error)) throw error;
155
+ return true;
156
+ }
157
+ }
158
+ async function queueKey(target, options) {
159
+ assertCurrent(options);
160
+ try {
161
+ const key = await fs2.realpath(target);
162
+ assertCurrent(options);
163
+ return key;
164
+ } catch (error) {
165
+ assertCurrent(options);
166
+ if (!isMissing(error)) throw error;
167
+ return path3.resolve(target);
168
+ }
169
+ }
170
+ async function assertCanonicalParent(target, options) {
171
+ const parent = path3.dirname(path3.resolve(target));
172
+ const suffix = [];
173
+ for (let current = parent; ; current = path3.dirname(current)) {
174
+ assertCurrent(options);
175
+ let canonical;
176
+ try {
177
+ canonical = await fs2.realpath(current);
178
+ } catch (error) {
179
+ assertCurrent(options);
180
+ if (!isMissing(error) || path3.dirname(current) === current) throw error;
181
+ suffix.unshift(path3.basename(current));
182
+ continue;
183
+ }
184
+ assertCurrent(options);
185
+ if (path3.join(canonical, ...suffix) !== parent)
186
+ throw new Error(
187
+ "Snapshot mutation through an aliased parent is unsafe; use the canonical agent/session path. Transaction evidence retained for review."
188
+ );
189
+ return;
190
+ }
191
+ }
192
+ async function assertCanonicalMutationTargets(targets, options) {
193
+ for (const target of targets) await assertCanonicalParent(target, options);
194
+ }
195
+ async function withTargetQueues(targets, options, action) {
196
+ await assertCanonicalMutationTargets(targets, options);
197
+ const keys = /* @__PURE__ */ new Set();
198
+ const missing = /* @__PURE__ */ new Set();
199
+ for (const target of targets) {
200
+ if (await targetMissing(target, options)) missing.add(target);
201
+ const key = await queueKey(target, options);
202
+ if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
203
+ keys.add(key);
204
+ }
205
+ const ordered = [...keys].sort();
206
+ async function acquire(index) {
207
+ assertCurrent(options);
208
+ const key = ordered[index];
209
+ if (key) return withFileMutationQueue(key, () => acquire(index + 1));
210
+ for (const target of targets) {
211
+ await assertCanonicalParent(target, options);
212
+ if (missing.has(target) && !await targetMissing(target, options))
213
+ throw new Error("Missing transaction target appeared during queue acquisition; review is required.");
214
+ if (!keys.has(await queueKey(target, options)))
215
+ throw new Error("Transaction queue identity changed; review is required.");
216
+ }
217
+ assertCurrent(options);
218
+ return action(keys);
219
+ }
220
+ return acquire(0);
221
+ }
222
+ async function withMissingTargetQueues(targets, heldKeys, options, action) {
223
+ const unique = [...new Set(targets)];
224
+ const requireAbsent = async () => {
225
+ for (const target of unique) {
226
+ await assertCanonicalParent(target, options);
227
+ if (!await targetMissing(target, options))
228
+ throw new Error("Snapshot target changed before destination queue reservation; evidence retained.");
229
+ }
230
+ };
231
+ const keys = unique.map((target) => path3.resolve(target)).filter((key) => !heldKeys.has(key)).sort();
232
+ await requireAbsent();
233
+ const combined = /* @__PURE__ */ new Set([...heldKeys, ...keys]);
234
+ async function acquire(index) {
235
+ assertCurrent(options);
236
+ const key = keys[index];
237
+ if (key) return withFileMutationQueue(key, () => acquire(index + 1));
238
+ await requireAbsent();
239
+ assertCurrent(options);
240
+ return action(combined);
241
+ }
242
+ return acquire(0);
243
+ }
244
+
245
+ // src/snapshot/snapshot-root-transition.ts
246
+ import fs3 from "node:fs/promises";
247
+ import path5 from "node:path";
132
248
 
133
249
  // src/snapshot/snapshot-transaction-plan.ts
134
250
  import { createHash } from "node:crypto";
135
- import path3 from "node:path";
251
+ import path4 from "node:path";
136
252
  function fileImage(content) {
137
253
  return `file:${createHash("sha256").update(content).digest("hex")}`;
138
254
  }
@@ -145,7 +261,7 @@ function indexTransactionPlan(plan, targets) {
145
261
  if (!writeImages.has(write.target)) writeImages.set(write.target, image2);
146
262
  for (const ancestor of ancestorTargets(write.target, targetIndex, false)) {
147
263
  if (ancestor !== write.target && isStrictDescendant(ancestor, write.target)) {
148
- postFiles.get(ancestor)?.push({ relative: path3.relative(ancestor, write.target), image: image2 });
264
+ postFiles.get(ancestor)?.push({ relative: path4.relative(ancestor, write.target), image: image2 });
149
265
  }
150
266
  }
151
267
  }
@@ -169,10 +285,10 @@ function indexTransactionPlan(plan, targets) {
169
285
  return { writeImages, postFiles, deletes, deletedWrites };
170
286
  }
171
287
  function isStrictDescendant(parent, target) {
172
- return path3.relative(path3.resolve(parent), path3.resolve(target)) !== "" && isPathInside(parent, target);
288
+ return path4.relative(path4.resolve(parent), path4.resolve(target)) !== "" && isPathInside(parent, target);
173
289
  }
174
290
  function pathKey(target) {
175
- const resolved = path3.resolve(target);
291
+ const resolved = path4.resolve(target);
176
292
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
177
293
  }
178
294
  function indexTargets(targets) {
@@ -186,10 +302,10 @@ function indexTargets(targets) {
186
302
  return index;
187
303
  }
188
304
  function* ancestorTargets(target, index, includeSelf = true) {
189
- const resolved = path3.resolve(target);
190
- for (let ancestor = includeSelf ? resolved : path3.dirname(resolved); ; ancestor = path3.dirname(ancestor)) {
305
+ const resolved = path4.resolve(target);
306
+ for (let ancestor = includeSelf ? resolved : path4.dirname(resolved); ; ancestor = path4.dirname(ancestor)) {
191
307
  yield* index.get(pathKey(ancestor)) ?? [];
192
- if (path3.dirname(ancestor) === ancestor) return;
308
+ if (path4.dirname(ancestor) === ancestor) return;
193
309
  }
194
310
  }
195
311
 
@@ -209,36 +325,36 @@ function rootFromSettings(root, bytes) {
209
325
  throw new Error("Invalid settings root-transition evidence.");
210
326
  const value = settings.sessionDir;
211
327
  if (value !== void 0 && typeof value !== "string") throw new Error("Invalid settings root-transition evidence.");
212
- return path4.resolve(sessionStorageRoot(root, value ? expandSessionDir(value) : void 0));
328
+ return path5.resolve(sessionStorageRoot(root, value ? expandSessionDir(value) : void 0));
213
329
  }
214
330
  async function beforeRoot(directory, root, entry, guards) {
215
331
  validateOwner(guards);
216
- if (entry.kind === "missing" && entry.beforeImage === "missing") return path4.join(root, "sessions");
332
+ if (entry.kind === "missing" && entry.beforeImage === "missing") return path5.join(root, "sessions");
217
333
  if (entry.kind !== "file") throw new Error("Root transition requires regular settings preimage evidence.");
218
- const backup = path4.join(directory, "before", entry.backupName);
219
- const stat = await fs2.lstat(backup);
334
+ const backup = path5.join(directory, "before", entry.backupName);
335
+ const stat = await fs3.lstat(backup);
220
336
  validateOwner(guards);
221
337
  if (!stat.isFile()) throw new Error("Invalid settings root-transition backup.");
222
- const bytes = await fs2.readFile(backup);
338
+ const bytes = await fs3.readFile(backup);
223
339
  validateOwner(guards);
224
340
  if (fileImage(bytes) !== entry.beforeImage) throw new Error("Changed settings root-transition backup.");
225
341
  return rootFromSettings(root, bytes);
226
342
  }
227
343
  async function prepareSessionRootTransition(directory, root, sessionRoot, entries, plan, guards = {}) {
228
- const settingsTarget = path4.join(root, "settings.json");
344
+ const settingsTarget = path5.join(root, "settings.json");
229
345
  const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
230
346
  const write = plan.writes.find((item) => item.target === settingsTarget || item.target === entry?.target);
231
347
  if (!entry || !entries.some((item) => item !== entry && isPathInside(sessionRoot, item.target))) return void 0;
232
348
  const deleted = !write && plan.deletes.includes(entry.target) && entry.afterImage === "missing";
233
349
  if (!write && !deleted) return void 0;
234
- const afterRoot = write ? rootFromSettings(root, write.content) : path4.join(root, "sessions");
350
+ const afterRoot = write ? rootFromSettings(root, write.content) : path5.join(root, "sessions");
235
351
  if (afterRoot !== sessionRoot) return void 0;
236
352
  const previous = await beforeRoot(directory, root, entry, guards);
237
353
  if (previous === sessionRoot) return void 0;
238
354
  if (write && fileImage(write.content) !== entry.afterImage) throw new Error("Changed settings transition postimage.");
239
355
  if (!write) return { beforeRoot: previous, settingsAfterBase64: null };
240
356
  validateOwner(guards);
241
- const handle = await fs2.open(path4.join(directory, "settings-after"), "wx", 384);
357
+ const handle = await fs3.open(path5.join(directory, "settings-after"), "wx", 384);
242
358
  try {
243
359
  validateOwner(guards);
244
360
  await handle.writeFile(write.content);
@@ -253,25 +369,25 @@ async function prepareSessionRootTransition(directory, root, sessionRoot, entrie
253
369
  return { beforeRoot: previous, settingsAfterFile: true };
254
370
  }
255
371
  function validateSessionRootTransition(value) {
256
- if (!value || typeof value.beforeRoot !== "string" || !path4.isAbsolute(value.beforeRoot) || path4.resolve(value.beforeRoot) !== value.beforeRoot || value.settingsAfterFile !== void 0 && value.settingsAfterFile !== true || (value.settingsAfterFile === true ? value.settingsAfterBase64 !== void 0 : value.settingsAfterBase64 !== null && (typeof value.settingsAfterBase64 !== "string" || value.settingsAfterBase64.length > 32 * 1024 * 1024 || Buffer.from(value.settingsAfterBase64, "base64").toString("base64") !== value.settingsAfterBase64)))
372
+ if (!value || typeof value.beforeRoot !== "string" || !path5.isAbsolute(value.beforeRoot) || path5.resolve(value.beforeRoot) !== value.beforeRoot || value.settingsAfterFile !== void 0 && value.settingsAfterFile !== true || (value.settingsAfterFile === true ? value.settingsAfterBase64 !== void 0 : value.settingsAfterBase64 !== null && (typeof value.settingsAfterBase64 !== "string" || value.settingsAfterBase64.length > 32 * 1024 * 1024 || Buffer.from(value.settingsAfterBase64, "base64").toString("base64") !== value.settingsAfterBase64)))
257
373
  throw new Error("Invalid session root-transition evidence.");
258
374
  }
259
375
  async function resolveTransitionSessionRoot(directory, root, sessionRoot, transition, entries, currentRoot, guards = {}) {
260
- const settingsTarget = path4.join(root, "settings.json");
376
+ const settingsTarget = path5.join(root, "settings.json");
261
377
  const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
262
378
  if (!entry) throw new Error("Missing settings root-transition evidence.");
263
379
  let after;
264
380
  if (transition.settingsAfterFile) {
265
- const file = path4.join(directory, "settings-after");
266
- const stat = await fs2.lstat(file);
381
+ const file = path5.join(directory, "settings-after");
382
+ const stat = await fs3.lstat(file);
267
383
  validateOwner(guards);
268
384
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || process.platform !== "win32" && (stat.mode & 63) !== 0)
269
385
  throw new Error("Unsafe settings root-transition postimage.");
270
- after = await fs2.readFile(file);
386
+ after = await fs3.readFile(file);
271
387
  validateOwner(guards);
272
388
  } else if (transition.settingsAfterBase64 !== null)
273
389
  after = Buffer.from(transition.settingsAfterBase64, "base64");
274
- if ((after ? fileImage(after) : "missing") !== entry.afterImage || (after ? rootFromSettings(root, after) : path4.join(root, "sessions")) !== sessionRoot || await beforeRoot(directory, root, entry, guards) !== transition.beforeRoot)
390
+ if ((after ? fileImage(after) : "missing") !== entry.afterImage || (after ? rootFromSettings(root, after) : path5.join(root, "sessions")) !== sessionRoot || await beforeRoot(directory, root, entry, guards) !== transition.beforeRoot)
275
391
  throw new Error("Inconsistent settings root-transition evidence.");
276
392
  if (currentRoot !== transition.beforeRoot && currentRoot !== sessionRoot)
277
393
  throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
@@ -283,13 +399,13 @@ var JOURNAL_VERSION = 4;
283
399
  async function applySnapshotTransaction(plan, options = {}) {
284
400
  options.validateMutation?.();
285
401
  await recoverPendingSnapshotTransactions(options);
286
- const coalesced = await coalesceCaseReplacements(path5.resolve(agentDir()), plan);
402
+ const coalesced = await coalesceCaseReplacements(path6.resolve(agentDir()), plan);
287
403
  options.validateMutation?.();
288
404
  options.signal?.throwIfAborted();
289
405
  const targets = [.../* @__PURE__ */ new Set([...coalesced.plan.deletes, ...coalesced.plan.writes.map((item) => item.target)])].sort();
290
406
  if (targets.length > 16384)
291
407
  throw new Error("Snapshot transaction exceeds its target bound; review a smaller transfer.");
292
- return withTargetQueues(targets, async () => {
408
+ return withTargetQueues(targets, options, async (heldKeys) => {
293
409
  options.signal?.throwIfAborted();
294
410
  if (targets.some(
295
411
  (target) => options.protectedTargets?.some(
@@ -313,7 +429,7 @@ async function applySnapshotTransaction(plan, options = {}) {
313
429
  for (const [target, expected] of options.expectedPreimages ?? []) {
314
430
  if (originalTargets.has(target)) continue;
315
431
  const variants = coalesced.plan.deletes.filter(
316
- (candidate) => candidate !== target && path5.dirname(candidate) === path5.dirname(target) && mergePathIdentity(candidate) === mergePathIdentity(target)
432
+ (candidate) => candidate !== target && path6.dirname(candidate) === path6.dirname(target) && mergePathIdentity(candidate) === mergePathIdentity(target)
317
433
  );
318
434
  if (variants.length > 1) throw new Error("Ambiguous reviewed file preimage spellings; review required.");
319
435
  const original = variants[0];
@@ -345,79 +461,87 @@ async function applySnapshotTransaction(plan, options = {}) {
345
461
  ...entry.afterTarget ? [[entry.afterTarget, entry]] : []
346
462
  ])
347
463
  );
464
+ const changesQueueKey = transaction.journal.entries.some(
465
+ (entry) => !heldKeys.has(entry.target) || entry.afterTarget && !heldKeys.has(entry.afterTarget)
466
+ );
348
467
  try {
349
468
  for (const target of transaction.deletes.sort((a, b) => a.length - b.length)) {
350
469
  options.signal?.throwIfAborted();
351
470
  const entry = entriesByTarget.get(target);
352
471
  if (!entry) throw new Error("Unowned transaction target.");
353
472
  await removeOwnedTarget(transaction.directory, entry, transaction.journal, options);
354
- await syncDirectory(path5.dirname(target));
473
+ await syncDirectory(path6.dirname(target));
355
474
  }
356
- await withCaseDestinationQueues([...coalesced.replacements], options, async () => {
357
- await retireRemovalIntent(
358
- transaction.directory,
359
- transaction.journal,
360
- transaction.journal.entries.filter(
361
- (entry) => entry.removalPending && (entry.afterImage !== "missing" || (entry.postFiles?.length ?? 0) > 0)
362
- ),
363
- options
364
- );
365
- const uninstalledWrites = new Set(transaction.deletedWrites);
366
- for (const item of plan.writes) {
367
- options.signal?.throwIfAborted();
368
- const entry = entriesByTarget.get(item.target);
369
- if (!entry) throw new Error("Unowned transaction target.");
370
- const deletedByThisCall = uninstalledWrites.delete(entry.target);
371
- await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
372
- options.validateMutation?.();
373
- options.signal?.throwIfAborted();
374
- await fs3.mkdir(path5.dirname(item.target), { recursive: true });
375
- options.validateMutation?.();
376
- options.signal?.throwIfAborted();
377
- const temp = path5.join(path5.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
378
- try {
379
- let mode = 384;
380
- try {
381
- const stat = await fs3.lstat(item.target);
382
- if (stat.isFile()) mode = stat.mode & 511;
383
- } catch (error) {
384
- if (error.code !== "ENOENT") throw error;
385
- }
386
- const handle = await fs3.open(temp, "wx", mode);
387
- try {
388
- await handle.writeFile(item.content);
389
- await handle.sync();
390
- } finally {
391
- await handle.close();
392
- }
475
+ await withMissingTargetQueues(
476
+ transaction.journal.entries.filter((entry) => entry.removalPending).flatMap((entry) => [entry.target, ...entry.afterTarget ? [entry.afterTarget] : []]),
477
+ heldKeys,
478
+ options,
479
+ async () => {
480
+ await retireRemovalIntent(
481
+ transaction.directory,
482
+ transaction.journal,
483
+ transaction.journal.entries.filter(
484
+ (entry) => entry.removalPending && (entry.afterImage !== "missing" || (entry.postFiles?.length ?? 0) > 0)
485
+ ),
486
+ options
487
+ );
488
+ const uninstalledWrites = new Set(transaction.deletedWrites);
489
+ for (const item of plan.writes) {
490
+ options.signal?.throwIfAborted();
491
+ const entry = entriesByTarget.get(item.target);
492
+ if (!entry) throw new Error("Unowned transaction target.");
493
+ const deletedByThisCall = uninstalledWrites.delete(entry.target);
393
494
  await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
495
+ options.validateMutation?.();
394
496
  options.signal?.throwIfAborted();
497
+ await fs4.mkdir(path6.dirname(item.target), { recursive: true });
395
498
  options.validateMutation?.();
396
- await fs3.rename(temp, item.target);
397
- await syncDirectory(path5.dirname(item.target));
398
- } finally {
399
- await fs3.rm(temp, { force: true });
499
+ options.signal?.throwIfAborted();
500
+ const temp = path6.join(path6.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
501
+ try {
502
+ let mode = 384;
503
+ try {
504
+ const stat = await fs4.lstat(item.target);
505
+ if (stat.isFile()) mode = stat.mode & 511;
506
+ } catch (error) {
507
+ if (error.code !== "ENOENT") throw error;
508
+ }
509
+ const handle = await fs4.open(temp, "wx", mode);
510
+ try {
511
+ await handle.writeFile(item.content);
512
+ await handle.sync();
513
+ } finally {
514
+ await handle.close();
515
+ }
516
+ await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
517
+ options.signal?.throwIfAborted();
518
+ options.validateMutation?.();
519
+ await fs4.rename(temp, item.target);
520
+ await syncDirectory(path6.dirname(item.target));
521
+ } finally {
522
+ await fs4.rm(temp, { force: true });
523
+ }
400
524
  }
525
+ options.validateMutation?.();
526
+ options.signal?.throwIfAborted();
527
+ await syncMutationParents(
528
+ targets,
529
+ [transaction.journal.root, ...transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : []],
530
+ options
531
+ );
532
+ options.validateMutation?.();
533
+ options.signal?.throwIfAborted();
401
534
  }
402
- options.validateMutation?.();
403
- options.signal?.throwIfAborted();
404
- await syncMutationParents(
405
- targets,
406
- [transaction.journal.root, ...transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : []],
407
- options
408
- );
409
- options.validateMutation?.();
410
- options.signal?.throwIfAborted();
411
- });
535
+ );
412
536
  } catch (error) {
413
537
  if (options.signal?.aborted)
414
538
  throw new Error("Snapshot apply cancelled; guarded transaction evidence retained for review.");
415
- if (coalesced.replacements.size)
416
- throw new Error("Case replacement apply failed; guarded transaction evidence retained for review.", {
539
+ if (changesQueueKey)
540
+ throw new Error("Snapshot apply failed; guarded transaction evidence retained for review.", {
417
541
  cause: error
418
542
  });
419
543
  try {
420
- await restoreTransaction(transaction.directory, transaction.journal, options);
544
+ await restoreTransaction(transaction.directory, transaction.journal, options, heldKeys);
421
545
  } catch (recoveryError) {
422
546
  throw new AggregateError(
423
547
  [error, recoveryError],
@@ -444,13 +568,13 @@ async function recoverSnapshotTransactionsOnStartup(options = {}) {
444
568
  async function recoverPendingSnapshotTransactions(options = {}) {
445
569
  for (const entry of (await pendingTransactionEntries()).sort((a, b) => a.name.localeCompare(b.name))) {
446
570
  if (!entry.isDirectory()) continue;
447
- const directory = path5.join(transactionRoot(), entry.name);
571
+ const directory = path6.join(transactionRoot(), entry.name);
448
572
  let journal;
449
573
  try {
450
- const file = path5.join(directory, "journal.json");
451
- const stat = await fs3.lstat(file);
574
+ const file = path6.join(directory, "journal.json");
575
+ const stat = await fs4.lstat(file);
452
576
  if (!stat.isFile() || stat.size > 32 * 1024 * 1024) throw new Error("Invalid journal file.");
453
- journal = JSON.parse(await fs3.readFile(file, "utf8"));
577
+ journal = JSON.parse(await fs4.readFile(file, "utf8"));
454
578
  } catch (error) {
455
579
  if (error.code === "ENOENT") continue;
456
580
  throw new Error("Cannot recover malformed pi-sync transaction; preserve private evidence for review.");
@@ -469,7 +593,7 @@ async function recoverPendingSnapshotTransactions(options = {}) {
469
593
  journal.sessionRoot,
470
594
  journal.sessionRootTransition,
471
595
  journal.entries,
472
- path5.resolve(sessionStorageRoot(journal.root, sessionDir)),
596
+ path6.resolve(sessionStorageRoot(journal.root, sessionDir)),
473
597
  options
474
598
  );
475
599
  options.validateMutation?.();
@@ -482,26 +606,19 @@ async function recoverPendingSnapshotTransactions(options = {}) {
482
606
  }
483
607
  await withTargetQueues(
484
608
  journal.entries.map((entry2) => entry2.target).sort(),
485
- () => restoreTransaction(directory, journal, { ...options, sessionDir })
609
+ options,
610
+ (heldKeys) => restoreTransaction(directory, journal, { ...options, sessionDir }, heldKeys)
486
611
  );
487
612
  }
488
613
  }
489
- async function pendingTransactionEntries() {
490
- try {
491
- return await fs3.readdir(transactionRoot(), { withFileTypes: true });
492
- } catch (error) {
493
- if (error.code === "ENOENT") return [];
494
- throw error;
495
- }
496
- }
497
614
  async function prepareTransaction(plan, targets, options, replacements = /* @__PURE__ */ new Map()) {
498
615
  const { sessionDir } = options;
499
- const root = path5.resolve(agentDir());
500
- const sessionRoot = sessionDir ? path5.resolve(sessionStorageRoot(root, sessionDir)) : void 0;
616
+ const root = path6.resolve(agentDir());
617
+ const sessionRoot = sessionDir ? path6.resolve(sessionStorageRoot(root, sessionDir)) : void 0;
501
618
  const indexed = indexTransactionPlan(plan, targets);
502
- const directory = path5.join(transactionRoot(), randomUUID());
503
- const backupDirectory = path5.join(directory, "before");
504
- await fs3.mkdir(backupDirectory, { recursive: true, mode: 448 });
619
+ const directory = path6.join(transactionRoot(), randomUUID());
620
+ const backupDirectory = path6.join(directory, "before");
621
+ await fs4.mkdir(backupDirectory, { recursive: true, mode: 448 });
505
622
  const entries = [];
506
623
  for (const [index, target] of targets.entries()) {
507
624
  options.signal?.throwIfAborted();
@@ -510,15 +627,15 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
510
627
  await assertSafeParents(root, sessionRoot, target);
511
628
  const entry = { target, backupName: `${index}`, kind: "missing" };
512
629
  if (replacements.has(target)) entry.afterTarget = replacements.get(target);
513
- const backup = path5.join(backupDirectory, entry.backupName);
630
+ const backup = path6.join(backupDirectory, entry.backupName);
514
631
  try {
515
- const stat = await fs3.lstat(target);
632
+ const stat = await fs4.lstat(target);
516
633
  if (stat.isSymbolicLink()) {
517
634
  entry.kind = "symlink";
518
- entry.linkTarget = await fs3.readlink(target);
635
+ entry.linkTarget = await fs4.readlink(target);
519
636
  } else if (stat.isDirectory()) {
520
637
  entry.kind = "directory";
521
- await fs3.cp(target, backup, {
638
+ await fs4.cp(target, backup, {
522
639
  recursive: true,
523
640
  dereference: false,
524
641
  verbatimSymlinks: true,
@@ -531,8 +648,8 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
531
648
  });
532
649
  } else if (stat.isFile()) {
533
650
  entry.kind = "file";
534
- await fs3.copyFile(target, backup);
535
- await fs3.chmod(backup, stat.mode);
651
+ await fs4.copyFile(target, backup);
652
+ await fs4.chmod(backup, stat.mode);
536
653
  } else throw new Error("Unsupported existing snapshot target.");
537
654
  } catch (error) {
538
655
  const code = error.code;
@@ -555,7 +672,7 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
555
672
  entries.push(entry);
556
673
  }
557
674
  await syncDirectory(backupDirectory);
558
- const effectiveRoot = sessionRoot ?? path5.join(root, "sessions");
675
+ const effectiveRoot = sessionRoot ?? path6.join(root, "sessions");
559
676
  const sessionRootTransition = await prepareSessionRootTransition(
560
677
  directory,
561
678
  root,
@@ -573,13 +690,32 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
573
690
  };
574
691
  options.signal?.throwIfAborted();
575
692
  options.validateMutation?.();
576
- await writeJson(path5.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
693
+ await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
577
694
  await syncDirectory(transactionRoot());
578
695
  await syncDirectory(stateDir());
579
- await syncDirectory(path5.dirname(stateDir()));
696
+ await syncDirectory(path6.dirname(stateDir()));
580
697
  return { directory, journal, deletes: indexed.deletes, deletedWrites: indexed.deletedWrites };
581
698
  }
582
- async function restoreTransaction(directory, journal, options) {
699
+ async function restoreTransaction(directory, journal, options, heldKeys) {
700
+ validateJournal(directory, journal, options.sessionDir);
701
+ const missingSpellings = [];
702
+ for (const entry of journal.entries) {
703
+ if (!entry.afterTarget) continue;
704
+ options.validateMutation?.();
705
+ options.signal?.throwIfAborted();
706
+ const spelling = await caseReplacementSpelling(entry.target, entry.afterTarget);
707
+ options.validateMutation?.();
708
+ options.signal?.throwIfAborted();
709
+ if (spelling === void 0) missingSpellings.push(entry.target, entry.afterTarget);
710
+ }
711
+ return withMissingTargetQueues(
712
+ missingSpellings,
713
+ heldKeys,
714
+ options,
715
+ (reservedKeys) => restoreQueuedTransaction(directory, journal, options, reservedKeys)
716
+ );
717
+ }
718
+ async function restoreQueuedTransaction(directory, journal, options, heldKeys) {
583
719
  validateJournal(directory, journal, options.sessionDir);
584
720
  options.validateMutation?.();
585
721
  options.signal?.throwIfAborted();
@@ -599,7 +735,7 @@ async function restoreTransaction(directory, journal, options) {
599
735
  await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
600
736
  options.validateMutation?.();
601
737
  options.signal?.throwIfAborted();
602
- await fs3.rm(recoveryStagePath(directory, entry), { recursive: true, force: true });
738
+ await fs4.rm(recoveryStagePath(directory, entry), { recursive: true, force: true });
603
739
  options.validateMutation?.();
604
740
  options.signal?.throwIfAborted();
605
741
  }
@@ -613,9 +749,9 @@ async function restoreTransaction(directory, journal, options) {
613
749
  if (entry.kind === "file") {
614
750
  const temporary2 = await prepareRecoveryStage(directory, journal, entry, options);
615
751
  try {
616
- await fs3.mkdir(path5.dirname(entry.target), { recursive: true });
617
- await fs3.copyFile(path5.join(directory, "before", entry.backupName), temporary2);
618
- const handle = await fs3.open(temporary2, process.platform === "win32" ? "r+" : "r");
752
+ await fs4.mkdir(path6.dirname(entry.target), { recursive: true });
753
+ await fs4.copyFile(path6.join(directory, "before", entry.backupName), temporary2);
754
+ const handle = await fs4.open(temporary2, process.platform === "win32" ? "r+" : "r");
619
755
  try {
620
756
  await handle.sync();
621
757
  } finally {
@@ -630,8 +766,9 @@ async function restoreTransaction(directory, journal, options) {
630
766
  await removeOwnedTarget(directory, entry, journal, options);
631
767
  } else if (verified.startsWith("directory:") && !await removeOwnedTarget(directory, entry, journal, options, before))
632
768
  continue;
633
- await withCaseDestinationQueues(
634
- needsCaseQueue && entry.afterTarget ? [[entry.afterTarget, entry.target]] : [],
769
+ await withMissingTargetQueues(
770
+ entry.afterTarget && (needsCaseQueue || verified === "missing") ? [entry.target, entry.afterTarget] : verified.startsWith("directory:") ? [entry.target] : [],
771
+ heldKeys,
635
772
  options,
636
773
  async () => {
637
774
  const atRename = await verifyTarget(directory, entry, journal);
@@ -643,12 +780,12 @@ async function restoreTransaction(directory, journal, options) {
643
780
  options.validateMutation?.();
644
781
  options.signal?.throwIfAborted();
645
782
  if (atInstall === before && await spellingMatches()) return;
646
- await fs3.rename(temporary2, entry.target);
647
- await syncDirectory(path5.dirname(entry.target));
783
+ await fs4.rename(temporary2, entry.target);
784
+ await syncDirectory(path6.dirname(entry.target));
648
785
  }
649
786
  );
650
787
  } finally {
651
- await fs3.rm(temporary2, { force: true });
788
+ await fs4.rm(temporary2, { force: true });
652
789
  }
653
790
  continue;
654
791
  }
@@ -658,10 +795,10 @@ async function restoreTransaction(directory, journal, options) {
658
795
  }
659
796
  const temporary = await prepareRecoveryStage(directory, journal, entry, options);
660
797
  try {
661
- await fs3.mkdir(path5.dirname(entry.target), { recursive: true });
798
+ await fs4.mkdir(path6.dirname(entry.target), { recursive: true });
662
799
  await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
663
800
  if (entry.kind === "directory") {
664
- await fs3.cp(path5.join(directory, "before", entry.backupName), temporary, {
801
+ await fs4.cp(path6.join(directory, "before", entry.backupName), temporary, {
665
802
  recursive: true,
666
803
  dereference: false,
667
804
  verbatimSymlinks: true,
@@ -672,25 +809,27 @@ async function restoreTransaction(directory, journal, options) {
672
809
  return true;
673
810
  }
674
811
  });
675
- } else if (entry.linkTarget !== void 0) await fs3.symlink(entry.linkTarget, temporary);
812
+ } else if (entry.linkTarget !== void 0) await fs4.symlink(entry.linkTarget, temporary);
676
813
  if (await image(temporary, true) !== before)
677
814
  throw new Error("Staged recovery image differs from the backup; evidence retained.");
678
815
  options.validateMutation?.();
679
816
  options.signal?.throwIfAborted();
680
817
  if (!await removeOwnedTarget(directory, entry, journal, options, before)) continue;
681
- const atRename = await verifyTarget(directory, entry, journal);
682
- options.validateMutation?.();
683
- options.signal?.throwIfAborted();
684
- if (atRename === before) continue;
685
- await retireRestorationIntent(directory, journal, entry, options);
686
- const atInstall = await verifyTarget(directory, entry, journal, atRename === "missing");
687
- options.validateMutation?.();
688
- options.signal?.throwIfAborted();
689
- if (atInstall === before) continue;
690
- await fs3.rename(temporary, entry.target);
691
- await syncDirectory(path5.dirname(entry.target));
818
+ await withMissingTargetQueues([entry.target], heldKeys, options, async () => {
819
+ const atRename = await verifyTarget(directory, entry, journal);
820
+ options.validateMutation?.();
821
+ options.signal?.throwIfAborted();
822
+ if (atRename === before) return;
823
+ await retireRestorationIntent(directory, journal, entry, options);
824
+ const atInstall = await verifyTarget(directory, entry, journal, atRename === "missing");
825
+ options.validateMutation?.();
826
+ options.signal?.throwIfAborted();
827
+ if (atInstall === before) return;
828
+ await fs4.rename(temporary, entry.target);
829
+ await syncDirectory(path6.dirname(entry.target));
830
+ });
692
831
  } finally {
693
- await fs3.rm(temporary, { recursive: true, force: true });
832
+ await fs4.rm(temporary, { recursive: true, force: true });
694
833
  }
695
834
  }
696
835
  await syncMutationParents(
@@ -735,13 +874,13 @@ async function removeOwnedTarget(directory, entry, journal, options, restoringBe
735
874
  affected.replacementStarted = false;
736
875
  }
737
876
  }
738
- await writeJson(path5.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
877
+ await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
739
878
  const verified = await verifyTarget(directory, entry, journal);
740
879
  options.validateMutation?.();
741
880
  options.signal?.throwIfAborted();
742
881
  if (restoringBefore !== void 0 && verified === restoringBefore) return false;
743
- await fs3.rm(entry.target, { recursive: true, force: true });
744
- await syncDirectory(path5.dirname(entry.target));
882
+ await fs4.rm(entry.target, { recursive: true, force: true });
883
+ await syncDirectory(path6.dirname(entry.target));
745
884
  return true;
746
885
  }
747
886
  function upgradeRemovalEvidence(journal) {
@@ -759,7 +898,7 @@ async function retireRemovalIntent(directory, journal, entries, options) {
759
898
  entry.removalPending = false;
760
899
  entry.replacementStarted = true;
761
900
  }
762
- await writeJson(path5.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
901
+ await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
763
902
  options.validateMutation?.();
764
903
  options.signal?.throwIfAborted();
765
904
  }
@@ -774,8 +913,8 @@ async function retireRestorationIntent(directory, journal, entry, options) {
774
913
  }
775
914
  function recoveryStagePath(directory, entry) {
776
915
  const token = createHash2("sha256").update(directory).update("\\0").update(entry.backupName).digest("hex");
777
- return path5.join(
778
- path5.dirname(entry.target),
916
+ return path6.join(
917
+ path6.dirname(entry.target),
779
918
  `.pi-sync.json.${token}.${entry.kind === "file" ? "restore" : "restore-tree"}`
780
919
  );
781
920
  }
@@ -783,7 +922,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
783
922
  const temporary = recoveryStagePath(directory, entry);
784
923
  if (!entry.recoveryStaged) {
785
924
  try {
786
- await fs3.lstat(temporary);
925
+ await fs4.lstat(temporary);
787
926
  throw new Error("Unowned recovery staging path; preserve evidence for review.");
788
927
  } catch (error) {
789
928
  if (error.code !== "ENOENT") throw error;
@@ -791,7 +930,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
791
930
  options.validateMutation?.();
792
931
  options.signal?.throwIfAborted();
793
932
  entry.recoveryStaged = true;
794
- await writeJson(path5.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
933
+ await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
795
934
  options.validateMutation?.();
796
935
  options.signal?.throwIfAborted();
797
936
  }
@@ -800,24 +939,24 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
800
939
  async function beforeImage(directory, entry) {
801
940
  if (entry.kind === "missing") return "missing";
802
941
  if (entry.kind === "symlink") return `symlink:${entry.linkTarget}`;
803
- const value = await image(path5.join(directory, "before", entry.backupName));
942
+ const value = await image(path6.join(directory, "before", entry.backupName));
804
943
  if (value === "missing") throw new Error("Transaction backup is missing; preserve evidence for review.");
805
944
  return value;
806
945
  }
807
946
  async function image(target, durable = false) {
808
947
  let stat;
809
948
  try {
810
- stat = await fs3.lstat(target);
949
+ stat = await fs4.lstat(target);
811
950
  } catch (error) {
812
951
  const code = error.code;
813
952
  if (code === "ENOENT" || code === "ENOTDIR") return "missing";
814
953
  throw error;
815
954
  }
816
- if (stat.isSymbolicLink()) return `symlink:${await fs3.readlink(target)}`;
955
+ if (stat.isSymbolicLink()) return `symlink:${await fs4.readlink(target)}`;
817
956
  if (stat.isFile()) {
818
- const content = await fs3.readFile(target);
957
+ const content = await fs4.readFile(target);
819
958
  if (durable) {
820
- const handle = await fs3.open(target, process.platform === "win32" ? "r+" : "r");
959
+ const handle = await fs4.open(target, process.platform === "win32" ? "r+" : "r");
821
960
  try {
822
961
  await handle.sync();
823
962
  } finally {
@@ -828,8 +967,8 @@ async function image(target, durable = false) {
828
967
  }
829
968
  if (!stat.isDirectory()) throw new Error("Unsupported transaction image.");
830
969
  const entries = [];
831
- for (const name of (await fs3.readdir(target)).sort())
832
- entries.push([name, await image(path5.join(target, name), durable)]);
970
+ for (const name of (await fs4.readdir(target)).sort())
971
+ entries.push([name, await image(path6.join(target, name), durable)]);
833
972
  if (durable) await syncDirectory(target);
834
973
  return `directory:${createHash2("sha256").update(JSON.stringify(entries)).digest("hex")}`;
835
974
  }
@@ -838,22 +977,22 @@ async function ownedPostTree(target, files, requireComplete) {
838
977
  const expected = new Map(files.map((file) => [mergePathIdentity(file.relative), file.image]));
839
978
  const prefixes = /* @__PURE__ */ new Set();
840
979
  for (const relative of expected.keys()) {
841
- for (let parent = path5.dirname(relative); parent !== "." && parent !== path5.dirname(parent); parent = path5.dirname(parent))
980
+ for (let parent = path6.dirname(relative); parent !== "." && parent !== path6.dirname(parent); parent = path6.dirname(parent))
842
981
  prefixes.add(parent);
843
982
  }
844
983
  const seen = /* @__PURE__ */ new Set();
845
984
  async function visit(directory) {
846
985
  let stat;
847
986
  try {
848
- stat = await fs3.lstat(directory);
987
+ stat = await fs4.lstat(directory);
849
988
  } catch {
850
989
  return false;
851
990
  }
852
991
  if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
853
- for (const name of await fs3.readdir(directory)) {
854
- const child = path5.join(directory, name);
855
- const relative = mergePathIdentity(path5.relative(target, child));
856
- const stat2 = await fs3.lstat(child);
992
+ for (const name of await fs4.readdir(directory)) {
993
+ const child = path6.join(directory, name);
994
+ const relative = mergePathIdentity(path6.relative(target, child));
995
+ const stat2 = await fs4.lstat(child);
857
996
  if (stat2.isDirectory() && !stat2.isSymbolicLink()) {
858
997
  if (!prefixes.has(relative) || !await visit(child)) return false;
859
998
  } else {
@@ -870,23 +1009,23 @@ function validateJournal(directory, journal, sessionDir) {
870
1009
  throw new Error("Unsupported pi-sync transaction journal; preserve evidence for review.");
871
1010
  if (journal.completed !== void 0 && (![6, 7].includes(journal.version) || journal.completed !== true))
872
1011
  throw new Error("Invalid transaction completion evidence.");
873
- const root = path5.resolve(agentDir());
874
- if (typeof journal.root !== "string" || path5.resolve(journal.root) !== root)
1012
+ const root = path6.resolve(agentDir());
1013
+ if (typeof journal.root !== "string" || path6.resolve(journal.root) !== root)
875
1014
  throw new Error("Transaction root no longer matches the Pi agent directory.");
876
1015
  if (journal.sessionRootTransition !== void 0) {
877
1016
  if (journal.version !== 7 || typeof journal.sessionRoot !== "string")
878
1017
  throw new Error("Invalid session root-transition version.");
879
1018
  validateSessionRootTransition(journal.sessionRootTransition);
880
1019
  } else if (journal.version === 7) throw new Error("Missing session root-transition evidence.");
881
- const trustedSessionRoot = sessionDir ? path5.resolve(sessionStorageRoot(root, sessionDir)) : path5.join(root, "sessions");
882
- if (journal.sessionRoot !== void 0 && (typeof journal.sessionRoot !== "string" || !path5.isAbsolute(journal.sessionRoot) || path5.resolve(journal.sessionRoot) !== journal.sessionRoot || !journal.completed && transactionHasSessionTargets(journal) && journal.sessionRoot !== trustedSessionRoot))
1020
+ const trustedSessionRoot = sessionDir ? path6.resolve(sessionStorageRoot(root, sessionDir)) : path6.join(root, "sessions");
1021
+ if (journal.sessionRoot !== void 0 && (typeof journal.sessionRoot !== "string" || !path6.isAbsolute(journal.sessionRoot) || path6.resolve(journal.sessionRoot) !== journal.sessionRoot || !journal.completed && transactionHasSessionTargets(journal) && journal.sessionRoot !== trustedSessionRoot))
883
1022
  throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
884
1023
  for (const entry of journal.entries) {
885
- if (!entry || typeof entry.target !== "string" || !path5.isAbsolute(entry.target) || path5.resolve(entry.target) !== entry.target || typeof entry.backupName !== "string" || !/^\d+$/u.test(entry.backupName) || !["missing", "file", "directory", "symlink"].includes(entry.kind))
1024
+ if (!entry || typeof entry.target !== "string" || !path6.isAbsolute(entry.target) || path6.resolve(entry.target) !== entry.target || typeof entry.backupName !== "string" || !/^\d+$/u.test(entry.backupName) || !["missing", "file", "directory", "symlink"].includes(entry.kind))
886
1025
  throw new Error("Invalid pi-sync transaction entry.");
887
- assertWithinRoot(directory, path5.join(directory, "before", entry.backupName));
1026
+ assertWithinRoot(directory, path6.join(directory, "before", entry.backupName));
888
1027
  assertAllowedTarget(root, journal.sessionRoot, entry.target);
889
- if (entry.afterTarget !== void 0 && (![5, 6, 7].includes(journal.version) || typeof entry.afterTarget !== "string" || !isCaseReplacement(root, entry.target, entry.afterTarget) || path5.resolve(entry.afterTarget) !== entry.afterTarget || entry.kind !== "file"))
1028
+ if (entry.afterTarget !== void 0 && (![5, 6, 7].includes(journal.version) || typeof entry.afterTarget !== "string" || !isCaseReplacement(root, entry.target, entry.afterTarget) || path6.resolve(entry.afterTarget) !== entry.afterTarget || entry.kind !== "file"))
890
1029
  throw new Error("Invalid transaction case replacement evidence.");
891
1030
  if (entry.afterTarget) assertAllowedTarget(root, journal.sessionRoot, entry.afterTarget);
892
1031
  if (journal.version >= 2 && ((entry.beforeImage === void 0 ? !journal.completed : typeof entry.beforeImage !== "string") || (entry.afterImage === void 0 ? !journal.completed : typeof entry.afterImage !== "string") || (entry.postFiles === void 0 ? !journal.completed : !Array.isArray(entry.postFiles))))
@@ -900,29 +1039,29 @@ function validateJournal(directory, journal, sessionDir) {
900
1039
  if (entry.removalPending && entry.replacementStarted)
901
1040
  throw new Error("Conflicting transaction replacement evidence.");
902
1041
  for (const file of entry.postFiles ?? []) {
903
- if (typeof file.relative !== "string" || typeof file.image !== "string" || !isStrictlyInside(entry.target, path5.resolve(entry.target, file.relative)))
1042
+ if (typeof file.relative !== "string" || typeof file.image !== "string" || !isStrictlyInside(entry.target, path6.resolve(entry.target, file.relative)))
904
1043
  throw new Error("Invalid transaction subtree evidence.");
905
1044
  }
906
1045
  }
907
1046
  }
908
1047
  function transactionHasSessionTargets(journal) {
909
- const root = path5.resolve(agentDir());
1048
+ const root = path6.resolve(agentDir());
910
1049
  return Array.isArray(journal.entries) && journal.entries.some(
911
- (entry) => typeof entry?.target === "string" && (!isPathInside(root, entry.target) || isPathInside(path5.join(root, "sessions"), entry.target) || typeof journal.sessionRoot === "string" && isPathInside(journal.sessionRoot, entry.target))
1050
+ (entry) => typeof entry?.target === "string" && (!isPathInside(root, entry.target) || isPathInside(path6.join(root, "sessions"), entry.target) || typeof journal.sessionRoot === "string" && isPathInside(journal.sessionRoot, entry.target))
912
1051
  );
913
1052
  }
914
1053
  function isStrictlyInside(root, target) {
915
- return path5.relative(path5.resolve(root), path5.resolve(target)) !== "" && isPathInside(root, target);
1054
+ return path6.relative(path6.resolve(root), path6.resolve(target)) !== "" && isPathInside(root, target);
916
1055
  }
917
1056
  function assertAllowedTarget(root, sessionRoot, target) {
918
- const resolved = path5.resolve(target);
1057
+ const resolved = path6.resolve(target);
919
1058
  if (isStrictlyInside(root, resolved)) {
920
- if (isDeniedPath(path5.relative(root, resolved))) throw new Error("Transaction targets denied private storage.");
1059
+ if (isDeniedPath(path6.relative(root, resolved))) throw new Error("Transaction targets denied private storage.");
921
1060
  assertWithinRoot(root, resolved);
922
1061
  return;
923
1062
  }
924
1063
  if (sessionRoot && isStrictlyInside(sessionRoot, resolved)) {
925
- if (isDeniedPath(`sessions/${path5.relative(sessionRoot, resolved)}`))
1064
+ if (isDeniedPath(`sessions/${path6.relative(sessionRoot, resolved)}`))
926
1065
  throw new Error("Transaction targets denied session storage.");
927
1066
  assertWithinRoot(sessionRoot, resolved);
928
1067
  return;
@@ -932,10 +1071,10 @@ function assertAllowedTarget(root, sessionRoot, target) {
932
1071
  async function assertSafeParents(root, sessionRoot, target) {
933
1072
  assertAllowedTarget(root, sessionRoot, target);
934
1073
  const boundary = isPathInside(root, target) ? root : sessionRoot;
935
- for (let parent = path5.dirname(target); boundary && mergePathIdentity(parent) !== mergePathIdentity(boundary); parent = path5.dirname(parent)) {
936
- if (path5.dirname(parent) === parent) throw new Error("Transaction parent did not reach its owned boundary.");
1074
+ for (let parent = path6.dirname(target); boundary && mergePathIdentity(parent) !== mergePathIdentity(boundary); parent = path6.dirname(parent)) {
1075
+ if (path6.dirname(parent) === parent) throw new Error("Transaction parent did not reach its owned boundary.");
937
1076
  try {
938
- const stat = await fs3.lstat(parent);
1077
+ const stat = await fs4.lstat(parent);
939
1078
  if (!stat.isDirectory() && !stat.isFile() || stat.isSymbolicLink())
940
1079
  throw new Error("Unsafe transaction parent.");
941
1080
  } catch (error) {
@@ -944,50 +1083,9 @@ async function assertSafeParents(root, sessionRoot, target) {
944
1083
  }
945
1084
  }
946
1085
  }
947
- async function withTargetQueues(targets, action) {
948
- const keys = /* @__PURE__ */ new Set();
949
- for (const target of targets) {
950
- let key = path5.resolve(target);
951
- try {
952
- key = await fs3.realpath(target);
953
- } catch (error) {
954
- const code = error.code;
955
- if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
956
- }
957
- if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
958
- keys.add(key);
959
- }
960
- async function acquire(index) {
961
- const target = targets[index];
962
- return target ? withFileMutationQueue(target, () => acquire(index + 1)) : action();
963
- }
964
- return acquire(0);
965
- }
966
- async function withCaseDestinationQueues(spellings, options, action) {
967
- if (!spellings.length) return action();
968
- const requireAbsent = async () => {
969
- for (const [before, after] of spellings) {
970
- options.validateMutation?.();
971
- options.signal?.throwIfAborted();
972
- const spelling = await caseReplacementSpelling(before, after);
973
- options.validateMutation?.();
974
- options.signal?.throwIfAborted();
975
- if (spelling !== void 0)
976
- throw new Error("Case replacement changed before destination queue reservation; evidence retained.");
977
- }
978
- };
979
- await requireAbsent();
980
- return withTargetQueues(
981
- spellings.map(([, after]) => after),
982
- async () => {
983
- await requireAbsent();
984
- return action();
985
- }
986
- );
987
- }
988
1086
  async function completeTransaction(directory, journal) {
989
1087
  await writeJson(
990
- path5.join(directory, "journal.json"),
1088
+ path6.join(directory, "journal.json"),
991
1089
  { ...journal, version: journal.sessionRootTransition ? 7 : 6, completed: true },
992
1090
  {
993
1091
  maxBytes: 32 * 1024 * 1024
@@ -996,18 +1094,16 @@ async function completeTransaction(directory, journal) {
996
1094
  await removeTransaction(directory);
997
1095
  }
998
1096
  async function removeTransaction(directory) {
999
- await fs3.rm(directory, { recursive: true, force: true });
1097
+ await fs4.rm(directory, { recursive: true, force: true });
1000
1098
  await syncDirectory(transactionRoot()).catch(() => {
1001
1099
  });
1002
1100
  }
1003
- function transactionRoot() {
1004
- return path5.join(stateDir(), "transactions");
1005
- }
1006
1101
 
1007
1102
  export {
1008
1103
  syncMutationParents,
1104
+ assertCanonicalMutationTargets,
1009
1105
  applySnapshotTransaction,
1010
1106
  recoverSnapshotTransactionsOnStartup,
1011
1107
  recoverPendingSnapshotTransactions
1012
1108
  };
1013
- //# sourceMappingURL=chunk-CRKTAPSL.ts.map
1109
+ //# sourceMappingURL=chunk-7A35ZAVO.ts.map