@narumitw/pi-sync 0.57.10 → 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 (147) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +5 -1
  3. package/dist/chunks/{agent-candidate-EYB35AGL.ts → agent-candidate-OCDPRI5G.ts} +11 -11
  4. package/dist/chunks/{agent-plan-KO43GTOR.ts → agent-plan-P5B5RGZ7.ts} +14 -14
  5. package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts → agent-refusal-retry-476YGNU5.ts} +10 -10
  6. package/dist/chunks/{agent-store-UT52DS7B.ts → agent-store-RJSMNOYP.ts} +7 -7
  7. package/dist/chunks/{agent-sync-VM3TXWA3.ts → agent-sync-EUK2CPSS.ts} +6 -6
  8. package/dist/chunks/{chunk-UCD7TQDT.ts → chunk-3IYAPMST.ts} +4 -4
  9. package/dist/chunks/{chunk-JXWTEJS5.ts → chunk-45COPAK7.ts} +10 -3
  10. package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
  11. package/dist/chunks/{chunk-DOJRPBQU.ts → chunk-5TSCBBPP.ts} +4 -4
  12. package/dist/chunks/{chunk-IMMMZUFC.ts → chunk-67INJPKR.ts} +4 -4
  13. package/dist/chunks/{chunk-CDQGSZL3.ts → chunk-6LD5KX55.ts} +6 -6
  14. package/dist/chunks/{chunk-MDUORMCK.ts → chunk-6O3EDBRL.ts} +2 -2
  15. package/dist/chunks/{chunk-AAPAHANE.ts → chunk-7A35ZAVO.ts} +320 -219
  16. package/dist/chunks/chunk-7A35ZAVO.ts.map +7 -0
  17. package/dist/chunks/{chunk-2VSN3MFX.ts → chunk-7OLLPPLU.ts} +29 -23
  18. package/dist/chunks/chunk-7OLLPPLU.ts.map +7 -0
  19. package/dist/chunks/{chunk-STVQQ5KG.ts → chunk-7ZIYU5QS.ts} +6 -6
  20. package/dist/chunks/{chunk-AO6EDUA6.ts → chunk-AKMV47YC.ts} +3 -3
  21. package/dist/chunks/{chunk-66GAUVDL.ts → chunk-BEST45H2.ts} +6 -6
  22. package/dist/chunks/{chunk-DO55XUNU.ts → chunk-CBLTWXAU.ts} +15 -5
  23. package/dist/chunks/chunk-CBLTWXAU.ts.map +7 -0
  24. package/dist/chunks/{chunk-PWA3YN5Z.ts → chunk-CPT2BN6N.ts} +2 -2
  25. package/dist/chunks/{chunk-73MDIVYJ.ts → chunk-DN4IA7MR.ts} +6 -6
  26. package/dist/chunks/{chunk-4RLATUP4.ts → chunk-DRTC4PKM.ts} +6 -6
  27. package/dist/chunks/{chunk-PHANBTVM.ts → chunk-ESKOPQVB.ts} +4 -4
  28. package/dist/chunks/{chunk-ODF2M4RF.ts → chunk-F7E3VFXU.ts} +5 -5
  29. package/dist/chunks/{chunk-YZ5BQNRZ.ts → chunk-FHE2DCA2.ts} +2 -2
  30. package/dist/chunks/{chunk-YZ5BQNRZ.ts.map → chunk-FHE2DCA2.ts.map} +2 -2
  31. package/dist/chunks/{chunk-MRIPZGPR.ts → chunk-FLHCOJCD.ts} +3 -3
  32. package/dist/chunks/{chunk-5WJQ6XOW.ts → chunk-GNYGNHFF.ts} +2 -2
  33. package/dist/chunks/{chunk-DFJGTVPX.ts → chunk-KYTT7KV4.ts} +2 -2
  34. package/dist/chunks/{chunk-UGSSZWQ6.ts → chunk-MKSMJPVA.ts} +2 -2
  35. package/dist/chunks/{chunk-RAMHGOX2.ts → chunk-OJARL2TG.ts} +4 -4
  36. package/dist/chunks/{chunk-BJXSCGQK.ts → chunk-OKPPZA4D.ts} +148 -89
  37. package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
  38. package/dist/chunks/{chunk-RX3WGWVL.ts → chunk-OVBAW7KV.ts} +177 -130
  39. package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
  40. package/dist/chunks/{chunk-U77DWNRH.ts → chunk-Q23JJW7E.ts} +3 -3
  41. package/dist/chunks/{chunk-QWV3XKNJ.ts → chunk-TIMWN2RH.ts} +5 -5
  42. package/dist/chunks/{chunk-Z5O2TLNF.ts → chunk-TZC7GDJE.ts} +7 -7
  43. package/dist/chunks/{chunk-TO2GMX3K.ts → chunk-XJHQPB3E.ts} +2 -2
  44. package/dist/chunks/{chunk-JY2RVOUL.ts → chunk-Y4UVXOLQ.ts} +5 -5
  45. package/dist/chunks/{chunk-M5IV3KL2.ts → chunk-ZDDBZH72.ts} +3 -3
  46. package/dist/chunks/{command-handler-FO6F2APH.ts → command-handler-F2E7A5T5.ts} +22 -17
  47. package/dist/chunks/command-handler-F2E7A5T5.ts.map +7 -0
  48. package/dist/chunks/{config-B37W7FXW.ts → config-HYG7G5EM.ts} +6 -4
  49. package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts → conflict-artifacts-J6WSDCWP.ts} +12 -12
  50. package/dist/chunks/{conflict-review-BQSHSJB3.ts → conflict-review-2QMO2K6Q.ts} +23 -23
  51. package/dist/chunks/{continuous-record-4DQ2EAZS.ts → continuous-record-AH7JGYKM.ts} +6 -6
  52. package/dist/chunks/{continuous-sync-ZT3UEQLK.ts → continuous-sync-VRMXCA5X.ts} +18 -18
  53. package/dist/chunks/{file-selection-FBLKM25V.ts → file-selection-IELTXSGZ.ts} +6 -7
  54. package/dist/chunks/{file-selection-FBLKM25V.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
  55. package/dist/chunks/{git-backend-7PCWECRN.ts → git-backend-KHD36W27.ts} +6 -7
  56. package/dist/chunks/{git-backend-7PCWECRN.ts.map → git-backend-KHD36W27.ts.map} +1 -1
  57. package/dist/chunks/{local-fields-B6JWLPAE.ts → local-fields-JEKAPZKD.ts} +6 -7
  58. package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
  59. package/dist/chunks/{manager-ui-DGRTG76I.ts → manager-ui-LM7DSJ2Y.ts} +103 -52
  60. package/dist/chunks/manager-ui-LM7DSJ2Y.ts.map +7 -0
  61. package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts → merge-baseline-store-65GKUMXI.ts} +6 -6
  62. package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts → remote-selection-ui-TD4C4W3J.ts} +16 -12
  63. package/dist/chunks/{s3-backend-CJXBEWWU.ts → s3-backend-L24UZ6IK.ts} +12 -10
  64. package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
  65. package/dist/chunks/{setup-switch-66GWMBNH.ts → setup-switch-PIPXYPCW.ts} +5 -5
  66. package/dist/chunks/{setup-wizard-OHZXJVQZ.ts → setup-wizard-RR5U3SUB.ts} +6 -6
  67. package/dist/chunks/{snapshot-VRHNAUUH.ts → snapshot-RGNDYT6A.ts} +5 -6
  68. package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts → snapshot-transaction-26BQO36S.ts} +8 -8
  69. package/dist/chunks/{sync-inspection-4WFQEJOE.ts → sync-inspection-MOMLGUTD.ts} +11 -11
  70. package/dist/chunks/{sync-operations-X4AOXMBT.ts → sync-operations-CV3TULLD.ts} +25 -25
  71. package/dist/chunks/{sync-state-SWLSMK4G.ts → sync-state-7L4NT7W2.ts} +7 -8
  72. package/dist/chunks/{webdav-backend-2Q5ZEBST.ts → webdav-backend-3ZCACILP.ts} +7 -8
  73. package/dist/chunks/{webdav-backend-2Q5ZEBST.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
  74. package/dist/index.ts +19 -19
  75. package/package.json +1 -1
  76. package/skills/configure-pi-sync/references/configuration.md +8 -1
  77. package/src/backends/s3/s3-client.ts +8 -1
  78. package/src/commands/command-execution.ts +8 -3
  79. package/src/paths.ts +6 -1
  80. package/src/settings/config-file.ts +205 -118
  81. package/src/settings/config.ts +1 -1
  82. package/src/settings/settings-management.ts +15 -1
  83. package/src/settings/settings-store.ts +0 -14
  84. package/src/snapshot/session-paths.ts +15 -2
  85. package/src/snapshot/snapshot-mutation-queues.ts +142 -0
  86. package/src/snapshot/snapshot-transaction.ts +122 -125
  87. package/src/sync/merge-apply.ts +6 -0
  88. package/src/ui/cancellable-operation.ts +36 -13
  89. package/src/ui/manager-state.ts +3 -1
  90. package/src/ui/manager-ui.ts +76 -24
  91. package/src/ui/remote-selection-ui.ts +131 -82
  92. package/src/ui/settings-ui.ts +2 -1
  93. package/dist/chunks/chunk-2VSN3MFX.ts.map +0 -7
  94. package/dist/chunks/chunk-AAPAHANE.ts.map +0 -7
  95. package/dist/chunks/chunk-BJXSCGQK.ts.map +0 -7
  96. package/dist/chunks/chunk-DO55XUNU.ts.map +0 -7
  97. package/dist/chunks/chunk-JXWTEJS5.ts.map +0 -7
  98. package/dist/chunks/chunk-RX3WGWVL.ts.map +0 -7
  99. package/dist/chunks/command-handler-FO6F2APH.ts.map +0 -7
  100. package/dist/chunks/manager-result-dispatcher-NODX3KKH.ts +0 -28
  101. package/dist/chunks/manager-ui-DGRTG76I.ts.map +0 -7
  102. package/dist/chunks/s3-backend-CJXBEWWU.ts.map +0 -7
  103. /package/dist/chunks/{agent-candidate-EYB35AGL.ts.map → agent-candidate-OCDPRI5G.ts.map} +0 -0
  104. /package/dist/chunks/{agent-plan-KO43GTOR.ts.map → agent-plan-P5B5RGZ7.ts.map} +0 -0
  105. /package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts.map → agent-refusal-retry-476YGNU5.ts.map} +0 -0
  106. /package/dist/chunks/{agent-store-UT52DS7B.ts.map → agent-store-RJSMNOYP.ts.map} +0 -0
  107. /package/dist/chunks/{agent-sync-VM3TXWA3.ts.map → agent-sync-EUK2CPSS.ts.map} +0 -0
  108. /package/dist/chunks/{chunk-UCD7TQDT.ts.map → chunk-3IYAPMST.ts.map} +0 -0
  109. /package/dist/chunks/{chunk-DOJRPBQU.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
  110. /package/dist/chunks/{chunk-IMMMZUFC.ts.map → chunk-67INJPKR.ts.map} +0 -0
  111. /package/dist/chunks/{chunk-CDQGSZL3.ts.map → chunk-6LD5KX55.ts.map} +0 -0
  112. /package/dist/chunks/{chunk-MDUORMCK.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
  113. /package/dist/chunks/{chunk-STVQQ5KG.ts.map → chunk-7ZIYU5QS.ts.map} +0 -0
  114. /package/dist/chunks/{chunk-AO6EDUA6.ts.map → chunk-AKMV47YC.ts.map} +0 -0
  115. /package/dist/chunks/{chunk-66GAUVDL.ts.map → chunk-BEST45H2.ts.map} +0 -0
  116. /package/dist/chunks/{chunk-PWA3YN5Z.ts.map → chunk-CPT2BN6N.ts.map} +0 -0
  117. /package/dist/chunks/{chunk-73MDIVYJ.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
  118. /package/dist/chunks/{chunk-4RLATUP4.ts.map → chunk-DRTC4PKM.ts.map} +0 -0
  119. /package/dist/chunks/{chunk-PHANBTVM.ts.map → chunk-ESKOPQVB.ts.map} +0 -0
  120. /package/dist/chunks/{chunk-ODF2M4RF.ts.map → chunk-F7E3VFXU.ts.map} +0 -0
  121. /package/dist/chunks/{chunk-MRIPZGPR.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
  122. /package/dist/chunks/{chunk-5WJQ6XOW.ts.map → chunk-GNYGNHFF.ts.map} +0 -0
  123. /package/dist/chunks/{chunk-DFJGTVPX.ts.map → chunk-KYTT7KV4.ts.map} +0 -0
  124. /package/dist/chunks/{chunk-UGSSZWQ6.ts.map → chunk-MKSMJPVA.ts.map} +0 -0
  125. /package/dist/chunks/{chunk-RAMHGOX2.ts.map → chunk-OJARL2TG.ts.map} +0 -0
  126. /package/dist/chunks/{chunk-U77DWNRH.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
  127. /package/dist/chunks/{chunk-QWV3XKNJ.ts.map → chunk-TIMWN2RH.ts.map} +0 -0
  128. /package/dist/chunks/{chunk-Z5O2TLNF.ts.map → chunk-TZC7GDJE.ts.map} +0 -0
  129. /package/dist/chunks/{chunk-TO2GMX3K.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
  130. /package/dist/chunks/{chunk-JY2RVOUL.ts.map → chunk-Y4UVXOLQ.ts.map} +0 -0
  131. /package/dist/chunks/{chunk-M5IV3KL2.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
  132. /package/dist/chunks/{config-B37W7FXW.ts.map → config-HYG7G5EM.ts.map} +0 -0
  133. /package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
  134. /package/dist/chunks/{conflict-review-BQSHSJB3.ts.map → conflict-review-2QMO2K6Q.ts.map} +0 -0
  135. /package/dist/chunks/{continuous-record-4DQ2EAZS.ts.map → continuous-record-AH7JGYKM.ts.map} +0 -0
  136. /package/dist/chunks/{continuous-sync-ZT3UEQLK.ts.map → continuous-sync-VRMXCA5X.ts.map} +0 -0
  137. /package/dist/chunks/{local-fields-B6JWLPAE.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
  138. /package/dist/chunks/{manager-result-dispatcher-NODX3KKH.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
  139. /package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
  140. /package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
  141. /package/dist/chunks/{setup-switch-66GWMBNH.ts.map → setup-switch-PIPXYPCW.ts.map} +0 -0
  142. /package/dist/chunks/{setup-wizard-OHZXJVQZ.ts.map → setup-wizard-RR5U3SUB.ts.map} +0 -0
  143. /package/dist/chunks/{snapshot-VRHNAUUH.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
  144. /package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
  145. /package/dist/chunks/{sync-inspection-4WFQEJOE.ts.map → sync-inspection-MOMLGUTD.ts.map} +0 -0
  146. /package/dist/chunks/{sync-operations-X4AOXMBT.ts.map → sync-operations-CV3TULLD.ts.map} +0 -0
  147. /package/dist/chunks/{sync-state-SWLSMK4G.ts.map → sync-state-7L4NT7W2.ts.map} +0 -0
@@ -6,10 +6,14 @@ import {
6
6
  } from "./chunk-5SFMBY5P.ts";
7
7
  import {
8
8
  withLock
9
- } from "./chunk-MDUORMCK.ts";
9
+ } from "./chunk-6O3EDBRL.ts";
10
10
  import {
11
11
  mergePathIdentity
12
- } from "./chunk-TO2GMX3K.ts";
12
+ } from "./chunk-XJHQPB3E.ts";
13
+ import {
14
+ syncDirectory,
15
+ writeJson
16
+ } from "./chunk-AR2DGDUI.ts";
13
17
  import {
14
18
  stateDir
15
19
  } from "./chunk-SWRVXVFD.ts";
@@ -18,22 +22,17 @@ import {
18
22
  configuredSessionDir,
19
23
  expandSessionDir,
20
24
  sessionStorageRoot
21
- } from "./chunk-DO55XUNU.ts";
22
- import {
23
- syncDirectory,
24
- writeJson
25
- } from "./chunk-AR2DGDUI.ts";
25
+ } from "./chunk-CBLTWXAU.ts";
26
26
  import {
27
27
  assertWithinRoot,
28
28
  isDeniedPath,
29
29
  isPathInside
30
- } from "./chunk-YZ5BQNRZ.ts";
30
+ } from "./chunk-FHE2DCA2.ts";
31
31
 
32
32
  // src/snapshot/snapshot-transaction.ts
33
33
  import { createHash as createHash2, randomUUID } from "node:crypto";
34
- import fs3 from "node:fs/promises";
35
- import path5 from "node:path";
36
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
34
+ import fs4 from "node:fs/promises";
35
+ import path6 from "node:path";
37
36
 
38
37
  // src/state/mutation-directory-sync.ts
39
38
  import path from "node:path";
@@ -132,13 +131,124 @@ async function coalesceCaseReplacements(root, plan) {
132
131
  };
133
132
  }
134
133
 
135
- // src/snapshot/snapshot-root-transition.ts
134
+ // src/snapshot/snapshot-mutation-queues.ts
136
135
  import fs2 from "node:fs/promises";
137
- 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";
138
248
 
139
249
  // src/snapshot/snapshot-transaction-plan.ts
140
250
  import { createHash } from "node:crypto";
141
- import path3 from "node:path";
251
+ import path4 from "node:path";
142
252
  function fileImage(content) {
143
253
  return `file:${createHash("sha256").update(content).digest("hex")}`;
144
254
  }
@@ -151,7 +261,7 @@ function indexTransactionPlan(plan, targets) {
151
261
  if (!writeImages.has(write.target)) writeImages.set(write.target, image2);
152
262
  for (const ancestor of ancestorTargets(write.target, targetIndex, false)) {
153
263
  if (ancestor !== write.target && isStrictDescendant(ancestor, write.target)) {
154
- 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 });
155
265
  }
156
266
  }
157
267
  }
@@ -175,10 +285,10 @@ function indexTransactionPlan(plan, targets) {
175
285
  return { writeImages, postFiles, deletes, deletedWrites };
176
286
  }
177
287
  function isStrictDescendant(parent, target) {
178
- 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);
179
289
  }
180
290
  function pathKey(target) {
181
- const resolved = path3.resolve(target);
291
+ const resolved = path4.resolve(target);
182
292
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
183
293
  }
184
294
  function indexTargets(targets) {
@@ -192,10 +302,10 @@ function indexTargets(targets) {
192
302
  return index;
193
303
  }
194
304
  function* ancestorTargets(target, index, includeSelf = true) {
195
- const resolved = path3.resolve(target);
196
- 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)) {
197
307
  yield* index.get(pathKey(ancestor)) ?? [];
198
- if (path3.dirname(ancestor) === ancestor) return;
308
+ if (path4.dirname(ancestor) === ancestor) return;
199
309
  }
200
310
  }
201
311
 
@@ -215,36 +325,36 @@ function rootFromSettings(root, bytes) {
215
325
  throw new Error("Invalid settings root-transition evidence.");
216
326
  const value = settings.sessionDir;
217
327
  if (value !== void 0 && typeof value !== "string") throw new Error("Invalid settings root-transition evidence.");
218
- return path4.resolve(sessionStorageRoot(root, value ? expandSessionDir(value) : void 0));
328
+ return path5.resolve(sessionStorageRoot(root, value ? expandSessionDir(value) : void 0));
219
329
  }
220
330
  async function beforeRoot(directory, root, entry, guards) {
221
331
  validateOwner(guards);
222
- 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");
223
333
  if (entry.kind !== "file") throw new Error("Root transition requires regular settings preimage evidence.");
224
- const backup = path4.join(directory, "before", entry.backupName);
225
- const stat = await fs2.lstat(backup);
334
+ const backup = path5.join(directory, "before", entry.backupName);
335
+ const stat = await fs3.lstat(backup);
226
336
  validateOwner(guards);
227
337
  if (!stat.isFile()) throw new Error("Invalid settings root-transition backup.");
228
- const bytes = await fs2.readFile(backup);
338
+ const bytes = await fs3.readFile(backup);
229
339
  validateOwner(guards);
230
340
  if (fileImage(bytes) !== entry.beforeImage) throw new Error("Changed settings root-transition backup.");
231
341
  return rootFromSettings(root, bytes);
232
342
  }
233
343
  async function prepareSessionRootTransition(directory, root, sessionRoot, entries, plan, guards = {}) {
234
- const settingsTarget = path4.join(root, "settings.json");
344
+ const settingsTarget = path5.join(root, "settings.json");
235
345
  const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
236
346
  const write = plan.writes.find((item) => item.target === settingsTarget || item.target === entry?.target);
237
347
  if (!entry || !entries.some((item) => item !== entry && isPathInside(sessionRoot, item.target))) return void 0;
238
348
  const deleted = !write && plan.deletes.includes(entry.target) && entry.afterImage === "missing";
239
349
  if (!write && !deleted) return void 0;
240
- const afterRoot = write ? rootFromSettings(root, write.content) : path4.join(root, "sessions");
350
+ const afterRoot = write ? rootFromSettings(root, write.content) : path5.join(root, "sessions");
241
351
  if (afterRoot !== sessionRoot) return void 0;
242
352
  const previous = await beforeRoot(directory, root, entry, guards);
243
353
  if (previous === sessionRoot) return void 0;
244
354
  if (write && fileImage(write.content) !== entry.afterImage) throw new Error("Changed settings transition postimage.");
245
355
  if (!write) return { beforeRoot: previous, settingsAfterBase64: null };
246
356
  validateOwner(guards);
247
- 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);
248
358
  try {
249
359
  validateOwner(guards);
250
360
  await handle.writeFile(write.content);
@@ -259,25 +369,25 @@ async function prepareSessionRootTransition(directory, root, sessionRoot, entrie
259
369
  return { beforeRoot: previous, settingsAfterFile: true };
260
370
  }
261
371
  function validateSessionRootTransition(value) {
262
- 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)))
263
373
  throw new Error("Invalid session root-transition evidence.");
264
374
  }
265
375
  async function resolveTransitionSessionRoot(directory, root, sessionRoot, transition, entries, currentRoot, guards = {}) {
266
- const settingsTarget = path4.join(root, "settings.json");
376
+ const settingsTarget = path5.join(root, "settings.json");
267
377
  const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
268
378
  if (!entry) throw new Error("Missing settings root-transition evidence.");
269
379
  let after;
270
380
  if (transition.settingsAfterFile) {
271
- const file = path4.join(directory, "settings-after");
272
- const stat = await fs2.lstat(file);
381
+ const file = path5.join(directory, "settings-after");
382
+ const stat = await fs3.lstat(file);
273
383
  validateOwner(guards);
274
384
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || process.platform !== "win32" && (stat.mode & 63) !== 0)
275
385
  throw new Error("Unsafe settings root-transition postimage.");
276
- after = await fs2.readFile(file);
386
+ after = await fs3.readFile(file);
277
387
  validateOwner(guards);
278
388
  } else if (transition.settingsAfterBase64 !== null)
279
389
  after = Buffer.from(transition.settingsAfterBase64, "base64");
280
- 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)
281
391
  throw new Error("Inconsistent settings root-transition evidence.");
282
392
  if (currentRoot !== transition.beforeRoot && currentRoot !== sessionRoot)
283
393
  throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
@@ -289,13 +399,13 @@ var JOURNAL_VERSION = 4;
289
399
  async function applySnapshotTransaction(plan, options = {}) {
290
400
  options.validateMutation?.();
291
401
  await recoverPendingSnapshotTransactions(options);
292
- const coalesced = await coalesceCaseReplacements(path5.resolve(agentDir()), plan);
402
+ const coalesced = await coalesceCaseReplacements(path6.resolve(agentDir()), plan);
293
403
  options.validateMutation?.();
294
404
  options.signal?.throwIfAborted();
295
405
  const targets = [.../* @__PURE__ */ new Set([...coalesced.plan.deletes, ...coalesced.plan.writes.map((item) => item.target)])].sort();
296
406
  if (targets.length > 16384)
297
407
  throw new Error("Snapshot transaction exceeds its target bound; review a smaller transfer.");
298
- return withTargetQueues(targets, async () => {
408
+ return withTargetQueues(targets, options, async (heldKeys) => {
299
409
  options.signal?.throwIfAborted();
300
410
  if (targets.some(
301
411
  (target) => options.protectedTargets?.some(
@@ -319,7 +429,7 @@ async function applySnapshotTransaction(plan, options = {}) {
319
429
  for (const [target, expected] of options.expectedPreimages ?? []) {
320
430
  if (originalTargets.has(target)) continue;
321
431
  const variants = coalesced.plan.deletes.filter(
322
- (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)
323
433
  );
324
434
  if (variants.length > 1) throw new Error("Ambiguous reviewed file preimage spellings; review required.");
325
435
  const original = variants[0];
@@ -351,79 +461,87 @@ async function applySnapshotTransaction(plan, options = {}) {
351
461
  ...entry.afterTarget ? [[entry.afterTarget, entry]] : []
352
462
  ])
353
463
  );
464
+ const changesQueueKey = transaction.journal.entries.some(
465
+ (entry) => !heldKeys.has(entry.target) || entry.afterTarget && !heldKeys.has(entry.afterTarget)
466
+ );
354
467
  try {
355
468
  for (const target of transaction.deletes.sort((a, b) => a.length - b.length)) {
356
469
  options.signal?.throwIfAborted();
357
470
  const entry = entriesByTarget.get(target);
358
471
  if (!entry) throw new Error("Unowned transaction target.");
359
472
  await removeOwnedTarget(transaction.directory, entry, transaction.journal, options);
360
- await syncDirectory(path5.dirname(target));
473
+ await syncDirectory(path6.dirname(target));
361
474
  }
362
- await withCaseDestinationQueues([...coalesced.replacements], options, async () => {
363
- await retireRemovalIntent(
364
- transaction.directory,
365
- transaction.journal,
366
- transaction.journal.entries.filter(
367
- (entry) => entry.removalPending && (entry.afterImage !== "missing" || (entry.postFiles?.length ?? 0) > 0)
368
- ),
369
- options
370
- );
371
- const uninstalledWrites = new Set(transaction.deletedWrites);
372
- for (const item of plan.writes) {
373
- options.signal?.throwIfAborted();
374
- const entry = entriesByTarget.get(item.target);
375
- if (!entry) throw new Error("Unowned transaction target.");
376
- const deletedByThisCall = uninstalledWrites.delete(entry.target);
377
- await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
378
- options.validateMutation?.();
379
- options.signal?.throwIfAborted();
380
- await fs3.mkdir(path5.dirname(item.target), { recursive: true });
381
- options.validateMutation?.();
382
- options.signal?.throwIfAborted();
383
- const temp = path5.join(path5.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
384
- try {
385
- let mode = 384;
386
- try {
387
- const stat = await fs3.lstat(item.target);
388
- if (stat.isFile()) mode = stat.mode & 511;
389
- } catch (error) {
390
- if (error.code !== "ENOENT") throw error;
391
- }
392
- const handle = await fs3.open(temp, "wx", mode);
393
- try {
394
- await handle.writeFile(item.content);
395
- await handle.sync();
396
- } finally {
397
- await handle.close();
398
- }
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);
399
494
  await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
495
+ options.validateMutation?.();
400
496
  options.signal?.throwIfAborted();
497
+ await fs4.mkdir(path6.dirname(item.target), { recursive: true });
401
498
  options.validateMutation?.();
402
- await fs3.rename(temp, item.target);
403
- await syncDirectory(path5.dirname(item.target));
404
- } finally {
405
- 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
+ }
406
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();
407
534
  }
408
- options.validateMutation?.();
409
- options.signal?.throwIfAborted();
410
- await syncMutationParents(
411
- targets,
412
- [transaction.journal.root, ...transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : []],
413
- options
414
- );
415
- options.validateMutation?.();
416
- options.signal?.throwIfAborted();
417
- });
535
+ );
418
536
  } catch (error) {
419
537
  if (options.signal?.aborted)
420
538
  throw new Error("Snapshot apply cancelled; guarded transaction evidence retained for review.");
421
- if (coalesced.replacements.size)
422
- 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.", {
423
541
  cause: error
424
542
  });
425
543
  try {
426
- await restoreTransaction(transaction.directory, transaction.journal, options);
544
+ await restoreTransaction(transaction.directory, transaction.journal, options, heldKeys);
427
545
  } catch (recoveryError) {
428
546
  throw new AggregateError(
429
547
  [error, recoveryError],
@@ -450,13 +568,13 @@ async function recoverSnapshotTransactionsOnStartup(options = {}) {
450
568
  async function recoverPendingSnapshotTransactions(options = {}) {
451
569
  for (const entry of (await pendingTransactionEntries()).sort((a, b) => a.name.localeCompare(b.name))) {
452
570
  if (!entry.isDirectory()) continue;
453
- const directory = path5.join(transactionRoot(), entry.name);
571
+ const directory = path6.join(transactionRoot(), entry.name);
454
572
  let journal;
455
573
  try {
456
- const file = path5.join(directory, "journal.json");
457
- const stat = await fs3.lstat(file);
574
+ const file = path6.join(directory, "journal.json");
575
+ const stat = await fs4.lstat(file);
458
576
  if (!stat.isFile() || stat.size > 32 * 1024 * 1024) throw new Error("Invalid journal file.");
459
- journal = JSON.parse(await fs3.readFile(file, "utf8"));
577
+ journal = JSON.parse(await fs4.readFile(file, "utf8"));
460
578
  } catch (error) {
461
579
  if (error.code === "ENOENT") continue;
462
580
  throw new Error("Cannot recover malformed pi-sync transaction; preserve private evidence for review.");
@@ -475,7 +593,7 @@ async function recoverPendingSnapshotTransactions(options = {}) {
475
593
  journal.sessionRoot,
476
594
  journal.sessionRootTransition,
477
595
  journal.entries,
478
- path5.resolve(sessionStorageRoot(journal.root, sessionDir)),
596
+ path6.resolve(sessionStorageRoot(journal.root, sessionDir)),
479
597
  options
480
598
  );
481
599
  options.validateMutation?.();
@@ -488,18 +606,19 @@ async function recoverPendingSnapshotTransactions(options = {}) {
488
606
  }
489
607
  await withTargetQueues(
490
608
  journal.entries.map((entry2) => entry2.target).sort(),
491
- () => restoreTransaction(directory, journal, { ...options, sessionDir })
609
+ options,
610
+ (heldKeys) => restoreTransaction(directory, journal, { ...options, sessionDir }, heldKeys)
492
611
  );
493
612
  }
494
613
  }
495
614
  async function prepareTransaction(plan, targets, options, replacements = /* @__PURE__ */ new Map()) {
496
615
  const { sessionDir } = options;
497
- const root = path5.resolve(agentDir());
498
- 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;
499
618
  const indexed = indexTransactionPlan(plan, targets);
500
- const directory = path5.join(transactionRoot(), randomUUID());
501
- const backupDirectory = path5.join(directory, "before");
502
- 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 });
503
622
  const entries = [];
504
623
  for (const [index, target] of targets.entries()) {
505
624
  options.signal?.throwIfAborted();
@@ -508,15 +627,15 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
508
627
  await assertSafeParents(root, sessionRoot, target);
509
628
  const entry = { target, backupName: `${index}`, kind: "missing" };
510
629
  if (replacements.has(target)) entry.afterTarget = replacements.get(target);
511
- const backup = path5.join(backupDirectory, entry.backupName);
630
+ const backup = path6.join(backupDirectory, entry.backupName);
512
631
  try {
513
- const stat = await fs3.lstat(target);
632
+ const stat = await fs4.lstat(target);
514
633
  if (stat.isSymbolicLink()) {
515
634
  entry.kind = "symlink";
516
- entry.linkTarget = await fs3.readlink(target);
635
+ entry.linkTarget = await fs4.readlink(target);
517
636
  } else if (stat.isDirectory()) {
518
637
  entry.kind = "directory";
519
- await fs3.cp(target, backup, {
638
+ await fs4.cp(target, backup, {
520
639
  recursive: true,
521
640
  dereference: false,
522
641
  verbatimSymlinks: true,
@@ -529,8 +648,8 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
529
648
  });
530
649
  } else if (stat.isFile()) {
531
650
  entry.kind = "file";
532
- await fs3.copyFile(target, backup);
533
- await fs3.chmod(backup, stat.mode);
651
+ await fs4.copyFile(target, backup);
652
+ await fs4.chmod(backup, stat.mode);
534
653
  } else throw new Error("Unsupported existing snapshot target.");
535
654
  } catch (error) {
536
655
  const code = error.code;
@@ -553,7 +672,7 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
553
672
  entries.push(entry);
554
673
  }
555
674
  await syncDirectory(backupDirectory);
556
- const effectiveRoot = sessionRoot ?? path5.join(root, "sessions");
675
+ const effectiveRoot = sessionRoot ?? path6.join(root, "sessions");
557
676
  const sessionRootTransition = await prepareSessionRootTransition(
558
677
  directory,
559
678
  root,
@@ -571,13 +690,32 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
571
690
  };
572
691
  options.signal?.throwIfAborted();
573
692
  options.validateMutation?.();
574
- 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 });
575
694
  await syncDirectory(transactionRoot());
576
695
  await syncDirectory(stateDir());
577
- await syncDirectory(path5.dirname(stateDir()));
696
+ await syncDirectory(path6.dirname(stateDir()));
578
697
  return { directory, journal, deletes: indexed.deletes, deletedWrites: indexed.deletedWrites };
579
698
  }
580
- 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) {
581
719
  validateJournal(directory, journal, options.sessionDir);
582
720
  options.validateMutation?.();
583
721
  options.signal?.throwIfAborted();
@@ -597,7 +735,7 @@ async function restoreTransaction(directory, journal, options) {
597
735
  await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
598
736
  options.validateMutation?.();
599
737
  options.signal?.throwIfAborted();
600
- await fs3.rm(recoveryStagePath(directory, entry), { recursive: true, force: true });
738
+ await fs4.rm(recoveryStagePath(directory, entry), { recursive: true, force: true });
601
739
  options.validateMutation?.();
602
740
  options.signal?.throwIfAborted();
603
741
  }
@@ -611,9 +749,9 @@ async function restoreTransaction(directory, journal, options) {
611
749
  if (entry.kind === "file") {
612
750
  const temporary2 = await prepareRecoveryStage(directory, journal, entry, options);
613
751
  try {
614
- await fs3.mkdir(path5.dirname(entry.target), { recursive: true });
615
- await fs3.copyFile(path5.join(directory, "before", entry.backupName), temporary2);
616
- 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");
617
755
  try {
618
756
  await handle.sync();
619
757
  } finally {
@@ -628,8 +766,9 @@ async function restoreTransaction(directory, journal, options) {
628
766
  await removeOwnedTarget(directory, entry, journal, options);
629
767
  } else if (verified.startsWith("directory:") && !await removeOwnedTarget(directory, entry, journal, options, before))
630
768
  continue;
631
- await withCaseDestinationQueues(
632
- 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,
633
772
  options,
634
773
  async () => {
635
774
  const atRename = await verifyTarget(directory, entry, journal);
@@ -641,12 +780,12 @@ async function restoreTransaction(directory, journal, options) {
641
780
  options.validateMutation?.();
642
781
  options.signal?.throwIfAborted();
643
782
  if (atInstall === before && await spellingMatches()) return;
644
- await fs3.rename(temporary2, entry.target);
645
- await syncDirectory(path5.dirname(entry.target));
783
+ await fs4.rename(temporary2, entry.target);
784
+ await syncDirectory(path6.dirname(entry.target));
646
785
  }
647
786
  );
648
787
  } finally {
649
- await fs3.rm(temporary2, { force: true });
788
+ await fs4.rm(temporary2, { force: true });
650
789
  }
651
790
  continue;
652
791
  }
@@ -656,10 +795,10 @@ async function restoreTransaction(directory, journal, options) {
656
795
  }
657
796
  const temporary = await prepareRecoveryStage(directory, journal, entry, options);
658
797
  try {
659
- await fs3.mkdir(path5.dirname(entry.target), { recursive: true });
798
+ await fs4.mkdir(path6.dirname(entry.target), { recursive: true });
660
799
  await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
661
800
  if (entry.kind === "directory") {
662
- await fs3.cp(path5.join(directory, "before", entry.backupName), temporary, {
801
+ await fs4.cp(path6.join(directory, "before", entry.backupName), temporary, {
663
802
  recursive: true,
664
803
  dereference: false,
665
804
  verbatimSymlinks: true,
@@ -670,25 +809,27 @@ async function restoreTransaction(directory, journal, options) {
670
809
  return true;
671
810
  }
672
811
  });
673
- } 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);
674
813
  if (await image(temporary, true) !== before)
675
814
  throw new Error("Staged recovery image differs from the backup; evidence retained.");
676
815
  options.validateMutation?.();
677
816
  options.signal?.throwIfAborted();
678
817
  if (!await removeOwnedTarget(directory, entry, journal, options, before)) continue;
679
- const atRename = await verifyTarget(directory, entry, journal);
680
- options.validateMutation?.();
681
- options.signal?.throwIfAborted();
682
- if (atRename === before) continue;
683
- await retireRestorationIntent(directory, journal, entry, options);
684
- const atInstall = await verifyTarget(directory, entry, journal, atRename === "missing");
685
- options.validateMutation?.();
686
- options.signal?.throwIfAborted();
687
- if (atInstall === before) continue;
688
- await fs3.rename(temporary, entry.target);
689
- 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
+ });
690
831
  } finally {
691
- await fs3.rm(temporary, { recursive: true, force: true });
832
+ await fs4.rm(temporary, { recursive: true, force: true });
692
833
  }
693
834
  }
694
835
  await syncMutationParents(
@@ -733,13 +874,13 @@ async function removeOwnedTarget(directory, entry, journal, options, restoringBe
733
874
  affected.replacementStarted = false;
734
875
  }
735
876
  }
736
- 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 });
737
878
  const verified = await verifyTarget(directory, entry, journal);
738
879
  options.validateMutation?.();
739
880
  options.signal?.throwIfAborted();
740
881
  if (restoringBefore !== void 0 && verified === restoringBefore) return false;
741
- await fs3.rm(entry.target, { recursive: true, force: true });
742
- await syncDirectory(path5.dirname(entry.target));
882
+ await fs4.rm(entry.target, { recursive: true, force: true });
883
+ await syncDirectory(path6.dirname(entry.target));
743
884
  return true;
744
885
  }
745
886
  function upgradeRemovalEvidence(journal) {
@@ -757,7 +898,7 @@ async function retireRemovalIntent(directory, journal, entries, options) {
757
898
  entry.removalPending = false;
758
899
  entry.replacementStarted = true;
759
900
  }
760
- 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 });
761
902
  options.validateMutation?.();
762
903
  options.signal?.throwIfAborted();
763
904
  }
@@ -772,8 +913,8 @@ async function retireRestorationIntent(directory, journal, entry, options) {
772
913
  }
773
914
  function recoveryStagePath(directory, entry) {
774
915
  const token = createHash2("sha256").update(directory).update("\\0").update(entry.backupName).digest("hex");
775
- return path5.join(
776
- path5.dirname(entry.target),
916
+ return path6.join(
917
+ path6.dirname(entry.target),
777
918
  `.pi-sync.json.${token}.${entry.kind === "file" ? "restore" : "restore-tree"}`
778
919
  );
779
920
  }
@@ -781,7 +922,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
781
922
  const temporary = recoveryStagePath(directory, entry);
782
923
  if (!entry.recoveryStaged) {
783
924
  try {
784
- await fs3.lstat(temporary);
925
+ await fs4.lstat(temporary);
785
926
  throw new Error("Unowned recovery staging path; preserve evidence for review.");
786
927
  } catch (error) {
787
928
  if (error.code !== "ENOENT") throw error;
@@ -789,7 +930,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
789
930
  options.validateMutation?.();
790
931
  options.signal?.throwIfAborted();
791
932
  entry.recoveryStaged = true;
792
- 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 });
793
934
  options.validateMutation?.();
794
935
  options.signal?.throwIfAborted();
795
936
  }
@@ -798,24 +939,24 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
798
939
  async function beforeImage(directory, entry) {
799
940
  if (entry.kind === "missing") return "missing";
800
941
  if (entry.kind === "symlink") return `symlink:${entry.linkTarget}`;
801
- const value = await image(path5.join(directory, "before", entry.backupName));
942
+ const value = await image(path6.join(directory, "before", entry.backupName));
802
943
  if (value === "missing") throw new Error("Transaction backup is missing; preserve evidence for review.");
803
944
  return value;
804
945
  }
805
946
  async function image(target, durable = false) {
806
947
  let stat;
807
948
  try {
808
- stat = await fs3.lstat(target);
949
+ stat = await fs4.lstat(target);
809
950
  } catch (error) {
810
951
  const code = error.code;
811
952
  if (code === "ENOENT" || code === "ENOTDIR") return "missing";
812
953
  throw error;
813
954
  }
814
- if (stat.isSymbolicLink()) return `symlink:${await fs3.readlink(target)}`;
955
+ if (stat.isSymbolicLink()) return `symlink:${await fs4.readlink(target)}`;
815
956
  if (stat.isFile()) {
816
- const content = await fs3.readFile(target);
957
+ const content = await fs4.readFile(target);
817
958
  if (durable) {
818
- const handle = await fs3.open(target, process.platform === "win32" ? "r+" : "r");
959
+ const handle = await fs4.open(target, process.platform === "win32" ? "r+" : "r");
819
960
  try {
820
961
  await handle.sync();
821
962
  } finally {
@@ -826,8 +967,8 @@ async function image(target, durable = false) {
826
967
  }
827
968
  if (!stat.isDirectory()) throw new Error("Unsupported transaction image.");
828
969
  const entries = [];
829
- for (const name of (await fs3.readdir(target)).sort())
830
- 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)]);
831
972
  if (durable) await syncDirectory(target);
832
973
  return `directory:${createHash2("sha256").update(JSON.stringify(entries)).digest("hex")}`;
833
974
  }
@@ -836,22 +977,22 @@ async function ownedPostTree(target, files, requireComplete) {
836
977
  const expected = new Map(files.map((file) => [mergePathIdentity(file.relative), file.image]));
837
978
  const prefixes = /* @__PURE__ */ new Set();
838
979
  for (const relative of expected.keys()) {
839
- 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))
840
981
  prefixes.add(parent);
841
982
  }
842
983
  const seen = /* @__PURE__ */ new Set();
843
984
  async function visit(directory) {
844
985
  let stat;
845
986
  try {
846
- stat = await fs3.lstat(directory);
987
+ stat = await fs4.lstat(directory);
847
988
  } catch {
848
989
  return false;
849
990
  }
850
991
  if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
851
- for (const name of await fs3.readdir(directory)) {
852
- const child = path5.join(directory, name);
853
- const relative = mergePathIdentity(path5.relative(target, child));
854
- 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);
855
996
  if (stat2.isDirectory() && !stat2.isSymbolicLink()) {
856
997
  if (!prefixes.has(relative) || !await visit(child)) return false;
857
998
  } else {
@@ -868,23 +1009,23 @@ function validateJournal(directory, journal, sessionDir) {
868
1009
  throw new Error("Unsupported pi-sync transaction journal; preserve evidence for review.");
869
1010
  if (journal.completed !== void 0 && (![6, 7].includes(journal.version) || journal.completed !== true))
870
1011
  throw new Error("Invalid transaction completion evidence.");
871
- const root = path5.resolve(agentDir());
872
- 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)
873
1014
  throw new Error("Transaction root no longer matches the Pi agent directory.");
874
1015
  if (journal.sessionRootTransition !== void 0) {
875
1016
  if (journal.version !== 7 || typeof journal.sessionRoot !== "string")
876
1017
  throw new Error("Invalid session root-transition version.");
877
1018
  validateSessionRootTransition(journal.sessionRootTransition);
878
1019
  } else if (journal.version === 7) throw new Error("Missing session root-transition evidence.");
879
- const trustedSessionRoot = sessionDir ? path5.resolve(sessionStorageRoot(root, sessionDir)) : path5.join(root, "sessions");
880
- 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))
881
1022
  throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
882
1023
  for (const entry of journal.entries) {
883
- 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))
884
1025
  throw new Error("Invalid pi-sync transaction entry.");
885
- assertWithinRoot(directory, path5.join(directory, "before", entry.backupName));
1026
+ assertWithinRoot(directory, path6.join(directory, "before", entry.backupName));
886
1027
  assertAllowedTarget(root, journal.sessionRoot, entry.target);
887
- 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"))
888
1029
  throw new Error("Invalid transaction case replacement evidence.");
889
1030
  if (entry.afterTarget) assertAllowedTarget(root, journal.sessionRoot, entry.afterTarget);
890
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))))
@@ -898,29 +1039,29 @@ function validateJournal(directory, journal, sessionDir) {
898
1039
  if (entry.removalPending && entry.replacementStarted)
899
1040
  throw new Error("Conflicting transaction replacement evidence.");
900
1041
  for (const file of entry.postFiles ?? []) {
901
- 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)))
902
1043
  throw new Error("Invalid transaction subtree evidence.");
903
1044
  }
904
1045
  }
905
1046
  }
906
1047
  function transactionHasSessionTargets(journal) {
907
- const root = path5.resolve(agentDir());
1048
+ const root = path6.resolve(agentDir());
908
1049
  return Array.isArray(journal.entries) && journal.entries.some(
909
- (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))
910
1051
  );
911
1052
  }
912
1053
  function isStrictlyInside(root, target) {
913
- 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);
914
1055
  }
915
1056
  function assertAllowedTarget(root, sessionRoot, target) {
916
- const resolved = path5.resolve(target);
1057
+ const resolved = path6.resolve(target);
917
1058
  if (isStrictlyInside(root, resolved)) {
918
- 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.");
919
1060
  assertWithinRoot(root, resolved);
920
1061
  return;
921
1062
  }
922
1063
  if (sessionRoot && isStrictlyInside(sessionRoot, resolved)) {
923
- if (isDeniedPath(`sessions/${path5.relative(sessionRoot, resolved)}`))
1064
+ if (isDeniedPath(`sessions/${path6.relative(sessionRoot, resolved)}`))
924
1065
  throw new Error("Transaction targets denied session storage.");
925
1066
  assertWithinRoot(sessionRoot, resolved);
926
1067
  return;
@@ -930,10 +1071,10 @@ function assertAllowedTarget(root, sessionRoot, target) {
930
1071
  async function assertSafeParents(root, sessionRoot, target) {
931
1072
  assertAllowedTarget(root, sessionRoot, target);
932
1073
  const boundary = isPathInside(root, target) ? root : sessionRoot;
933
- for (let parent = path5.dirname(target); boundary && mergePathIdentity(parent) !== mergePathIdentity(boundary); parent = path5.dirname(parent)) {
934
- 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.");
935
1076
  try {
936
- const stat = await fs3.lstat(parent);
1077
+ const stat = await fs4.lstat(parent);
937
1078
  if (!stat.isDirectory() && !stat.isFile() || stat.isSymbolicLink())
938
1079
  throw new Error("Unsafe transaction parent.");
939
1080
  } catch (error) {
@@ -942,50 +1083,9 @@ async function assertSafeParents(root, sessionRoot, target) {
942
1083
  }
943
1084
  }
944
1085
  }
945
- async function withTargetQueues(targets, action) {
946
- const keys = /* @__PURE__ */ new Set();
947
- for (const target of targets) {
948
- let key = path5.resolve(target);
949
- try {
950
- key = await fs3.realpath(target);
951
- } catch (error) {
952
- const code = error.code;
953
- if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
954
- }
955
- if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
956
- keys.add(key);
957
- }
958
- async function acquire(index) {
959
- const target = targets[index];
960
- return target ? withFileMutationQueue(target, () => acquire(index + 1)) : action();
961
- }
962
- return acquire(0);
963
- }
964
- async function withCaseDestinationQueues(spellings, options, action) {
965
- if (!spellings.length) return action();
966
- const requireAbsent = async () => {
967
- for (const [before, after] of spellings) {
968
- options.validateMutation?.();
969
- options.signal?.throwIfAborted();
970
- const spelling = await caseReplacementSpelling(before, after);
971
- options.validateMutation?.();
972
- options.signal?.throwIfAborted();
973
- if (spelling !== void 0)
974
- throw new Error("Case replacement changed before destination queue reservation; evidence retained.");
975
- }
976
- };
977
- await requireAbsent();
978
- return withTargetQueues(
979
- spellings.map(([, after]) => after),
980
- async () => {
981
- await requireAbsent();
982
- return action();
983
- }
984
- );
985
- }
986
1086
  async function completeTransaction(directory, journal) {
987
1087
  await writeJson(
988
- path5.join(directory, "journal.json"),
1088
+ path6.join(directory, "journal.json"),
989
1089
  { ...journal, version: journal.sessionRootTransition ? 7 : 6, completed: true },
990
1090
  {
991
1091
  maxBytes: 32 * 1024 * 1024
@@ -994,15 +1094,16 @@ async function completeTransaction(directory, journal) {
994
1094
  await removeTransaction(directory);
995
1095
  }
996
1096
  async function removeTransaction(directory) {
997
- await fs3.rm(directory, { recursive: true, force: true });
1097
+ await fs4.rm(directory, { recursive: true, force: true });
998
1098
  await syncDirectory(transactionRoot()).catch(() => {
999
1099
  });
1000
1100
  }
1001
1101
 
1002
1102
  export {
1003
1103
  syncMutationParents,
1104
+ assertCanonicalMutationTargets,
1004
1105
  applySnapshotTransaction,
1005
1106
  recoverSnapshotTransactionsOnStartup,
1006
1107
  recoverPendingSnapshotTransactions
1007
1108
  };
1008
- //# sourceMappingURL=chunk-AAPAHANE.ts.map
1109
+ //# sourceMappingURL=chunk-7A35ZAVO.ts.map