@narumitw/pi-sync 0.57.10 → 0.57.12

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 (173) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +32 -22
  3. package/dist/chunks/{agent-candidate-EYB35AGL.ts → agent-candidate-U2NU3GUX.ts} +11 -11
  4. package/dist/chunks/{agent-plan-KO43GTOR.ts → agent-plan-3YXDIJXO.ts} +25 -22
  5. package/dist/chunks/agent-plan-3YXDIJXO.ts.map +7 -0
  6. package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts → agent-refusal-retry-476YGNU5.ts} +10 -10
  7. package/dist/chunks/{agent-runner-WMSCKSCB.ts → agent-runner-FCRV2Z75.ts} +11 -10
  8. package/dist/chunks/agent-runner-FCRV2Z75.ts.map +7 -0
  9. package/dist/chunks/{agent-store-UT52DS7B.ts → agent-store-7GINKTS7.ts} +8 -8
  10. package/dist/chunks/{agent-store-UT52DS7B.ts.map → agent-store-7GINKTS7.ts.map} +2 -2
  11. package/dist/chunks/{agent-sync-VM3TXWA3.ts → agent-sync-P6AWQXGK.ts} +6 -6
  12. package/dist/chunks/{chunk-JXWTEJS5.ts → chunk-45COPAK7.ts} +10 -3
  13. package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
  14. package/dist/chunks/{chunk-DOJRPBQU.ts → chunk-5TSCBBPP.ts} +4 -4
  15. package/dist/chunks/{chunk-CDQGSZL3.ts → chunk-6LD5KX55.ts} +6 -6
  16. package/dist/chunks/{chunk-MDUORMCK.ts → chunk-6O3EDBRL.ts} +2 -2
  17. package/dist/chunks/{chunk-AAPAHANE.ts → chunk-7A35ZAVO.ts} +320 -219
  18. package/dist/chunks/chunk-7A35ZAVO.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-IMMMZUFC.ts → chunk-CQD2TJHE.ts} +7 -7
  25. package/dist/chunks/{chunk-IMMMZUFC.ts.map → chunk-CQD2TJHE.ts.map} +2 -2
  26. package/dist/chunks/{chunk-73MDIVYJ.ts → chunk-DN4IA7MR.ts} +6 -6
  27. package/dist/chunks/{chunk-PHANBTVM.ts → chunk-ESKOPQVB.ts} +4 -4
  28. package/dist/chunks/{chunk-YZ5BQNRZ.ts → chunk-FHE2DCA2.ts} +2 -2
  29. package/dist/chunks/{chunk-YZ5BQNRZ.ts.map → chunk-FHE2DCA2.ts.map} +2 -2
  30. package/dist/chunks/{chunk-MRIPZGPR.ts → chunk-FLHCOJCD.ts} +3 -3
  31. package/dist/chunks/{chunk-5WJQ6XOW.ts → chunk-GNYGNHFF.ts} +2 -2
  32. package/dist/chunks/{chunk-DFJGTVPX.ts → chunk-KYTT7KV4.ts} +2 -2
  33. package/dist/chunks/{chunk-UGSSZWQ6.ts → chunk-MKSMJPVA.ts} +2 -2
  34. package/dist/chunks/{chunk-2VSN3MFX.ts → chunk-MMQIEDZ6.ts} +33 -27
  35. package/dist/chunks/chunk-MMQIEDZ6.ts.map +7 -0
  36. package/dist/chunks/{chunk-RAMHGOX2.ts → chunk-OJARL2TG.ts} +4 -4
  37. package/dist/chunks/{chunk-BJXSCGQK.ts → chunk-OKPPZA4D.ts} +148 -89
  38. package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
  39. package/dist/chunks/{chunk-RX3WGWVL.ts → chunk-OVBAW7KV.ts} +177 -130
  40. package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
  41. package/dist/chunks/{chunk-U77DWNRH.ts → chunk-Q23JJW7E.ts} +3 -3
  42. package/dist/chunks/{chunk-NLWS55B6.ts → chunk-QPC5HT3A.ts} +2 -2
  43. package/dist/chunks/{chunk-NLWS55B6.ts.map → chunk-QPC5HT3A.ts.map} +1 -1
  44. package/dist/chunks/{chunk-4RLATUP4.ts → chunk-S5OCYVPS.ts} +7 -7
  45. package/dist/chunks/{chunk-4RLATUP4.ts.map → chunk-S5OCYVPS.ts.map} +2 -2
  46. package/dist/chunks/{chunk-QWV3XKNJ.ts → chunk-TIMWN2RH.ts} +5 -5
  47. package/dist/chunks/{chunk-Z5O2TLNF.ts → chunk-TZC7GDJE.ts} +7 -7
  48. package/dist/chunks/{chunk-UCD7TQDT.ts → chunk-U6GXGY5G.ts} +5 -5
  49. package/dist/chunks/{chunk-PWA3YN5Z.ts → chunk-XEODG2PE.ts} +3 -3
  50. package/dist/chunks/{chunk-TO2GMX3K.ts → chunk-XJHQPB3E.ts} +2 -2
  51. package/dist/chunks/{chunk-JY2RVOUL.ts → chunk-Y4UVXOLQ.ts} +5 -5
  52. package/dist/chunks/{chunk-ODF2M4RF.ts → chunk-ZC3IHFNM.ts} +8 -8
  53. package/dist/chunks/{chunk-ODF2M4RF.ts.map → chunk-ZC3IHFNM.ts.map} +2 -2
  54. package/dist/chunks/{chunk-M5IV3KL2.ts → chunk-ZDDBZH72.ts} +3 -3
  55. package/dist/chunks/{command-handler-FO6F2APH.ts → command-handler-VKLZBRLO.ts} +26 -21
  56. package/dist/chunks/command-handler-VKLZBRLO.ts.map +7 -0
  57. package/dist/chunks/{config-B37W7FXW.ts → config-HYG7G5EM.ts} +6 -4
  58. package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts → conflict-artifacts-J6WSDCWP.ts} +12 -12
  59. package/dist/chunks/{conflict-review-BQSHSJB3.ts → conflict-review-GXA6Y7KY.ts} +23 -23
  60. package/dist/chunks/{continuous-record-4DQ2EAZS.ts → continuous-record-EPHGR2DU.ts} +6 -6
  61. package/dist/chunks/{continuous-sync-ZT3UEQLK.ts → continuous-sync-TTOSJCQU.ts} +18 -18
  62. package/dist/chunks/{file-selection-FBLKM25V.ts → file-selection-IELTXSGZ.ts} +6 -7
  63. package/dist/chunks/{file-selection-FBLKM25V.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
  64. package/dist/chunks/{git-backend-7PCWECRN.ts → git-backend-KHD36W27.ts} +6 -7
  65. package/dist/chunks/{git-backend-7PCWECRN.ts.map → git-backend-KHD36W27.ts.map} +1 -1
  66. package/dist/chunks/{local-fields-B6JWLPAE.ts → local-fields-JEKAPZKD.ts} +6 -7
  67. package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
  68. package/dist/chunks/{manager-ui-DGRTG76I.ts → manager-ui-INVMAFEN.ts} +245 -224
  69. package/dist/chunks/manager-ui-INVMAFEN.ts.map +7 -0
  70. package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts → merge-baseline-store-65GKUMXI.ts} +6 -6
  71. package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts → remote-selection-ui-TD4C4W3J.ts} +16 -12
  72. package/dist/chunks/{s3-backend-CJXBEWWU.ts → s3-backend-L24UZ6IK.ts} +12 -10
  73. package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
  74. package/dist/chunks/{setup-switch-66GWMBNH.ts → setup-switch-KLGESLQX.ts} +6 -6
  75. package/dist/chunks/{setup-wizard-OHZXJVQZ.ts → setup-wizard-ZQAK35BP.ts} +7 -7
  76. package/dist/chunks/{snapshot-VRHNAUUH.ts → snapshot-RGNDYT6A.ts} +5 -6
  77. package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts → snapshot-transaction-26BQO36S.ts} +8 -8
  78. package/dist/chunks/{sync-inspection-4WFQEJOE.ts → sync-inspection-MOMLGUTD.ts} +11 -11
  79. package/dist/chunks/{sync-operations-X4AOXMBT.ts → sync-operations-TMGQHBF5.ts} +26 -26
  80. package/dist/chunks/{sync-operations-X4AOXMBT.ts.map → sync-operations-TMGQHBF5.ts.map} +2 -2
  81. package/dist/chunks/{sync-state-SWLSMK4G.ts → sync-state-7L4NT7W2.ts} +7 -8
  82. package/dist/chunks/{webdav-backend-2Q5ZEBST.ts → webdav-backend-3ZCACILP.ts} +7 -8
  83. package/dist/chunks/{webdav-backend-2Q5ZEBST.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
  84. package/dist/index.ts +20 -20
  85. package/docs/agent-continuous-hardening.md +5 -5
  86. package/docs/agent-mode-verification.md +3 -3
  87. package/docs/agent-quality-verification.md +3 -3
  88. package/docs/continuous-sync-verification.md +2 -2
  89. package/docs/settings.md +9 -5
  90. package/docs/tui-contract.md +20 -3
  91. package/package.json +3 -3
  92. package/skills/configure-pi-sync/SKILL.md +3 -1
  93. package/skills/configure-pi-sync/references/configuration.md +56 -38
  94. package/src/backends/s3/s3-client.ts +8 -1
  95. package/src/commands/command-execution.ts +11 -6
  96. package/src/commands/command.ts +1 -1
  97. package/src/paths.ts +6 -1
  98. package/src/settings/config-file.ts +205 -118
  99. package/src/settings/config.ts +1 -1
  100. package/src/settings/settings-management.ts +15 -1
  101. package/src/settings/settings-store.ts +0 -14
  102. package/src/settings/settings-types.ts +1 -1
  103. package/src/snapshot/session-paths.ts +15 -2
  104. package/src/snapshot/snapshot-mutation-queues.ts +142 -0
  105. package/src/snapshot/snapshot-transaction.ts +122 -125
  106. package/src/sync/agent-candidate.ts +3 -3
  107. package/src/sync/agent-plan.ts +11 -8
  108. package/src/sync/agent-runner.ts +10 -9
  109. package/src/sync/agent-store.ts +1 -1
  110. package/src/sync/agent-sync.ts +2 -2
  111. package/src/sync/continuous-record.ts +1 -1
  112. package/src/sync/merge-apply.ts +6 -0
  113. package/src/sync/merged-sync.ts +4 -4
  114. package/src/sync/sync-mutations.ts +1 -1
  115. package/src/ui/agent-mode-setting.ts +6 -6
  116. package/src/ui/cancellable-operation.ts +36 -13
  117. package/src/ui/manager-state.ts +4 -2
  118. package/src/ui/manager-ui.ts +76 -24
  119. package/src/ui/remote-selection-ui.ts +131 -82
  120. package/src/ui/settings-ui.ts +21 -56
  121. package/src/ui/setup/setup-switcher.ts +2 -2
  122. package/src/ui/sync-setups-ui.ts +2 -2
  123. package/dist/chunks/agent-plan-KO43GTOR.ts.map +0 -7
  124. package/dist/chunks/agent-runner-WMSCKSCB.ts.map +0 -7
  125. package/dist/chunks/chunk-2VSN3MFX.ts.map +0 -7
  126. package/dist/chunks/chunk-AAPAHANE.ts.map +0 -7
  127. package/dist/chunks/chunk-BJXSCGQK.ts.map +0 -7
  128. package/dist/chunks/chunk-DO55XUNU.ts.map +0 -7
  129. package/dist/chunks/chunk-JXWTEJS5.ts.map +0 -7
  130. package/dist/chunks/chunk-RX3WGWVL.ts.map +0 -7
  131. package/dist/chunks/command-handler-FO6F2APH.ts.map +0 -7
  132. package/dist/chunks/manager-result-dispatcher-NODX3KKH.ts +0 -28
  133. package/dist/chunks/manager-ui-DGRTG76I.ts.map +0 -7
  134. package/dist/chunks/s3-backend-CJXBEWWU.ts.map +0 -7
  135. /package/dist/chunks/{agent-candidate-EYB35AGL.ts.map → agent-candidate-U2NU3GUX.ts.map} +0 -0
  136. /package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts.map → agent-refusal-retry-476YGNU5.ts.map} +0 -0
  137. /package/dist/chunks/{agent-sync-VM3TXWA3.ts.map → agent-sync-P6AWQXGK.ts.map} +0 -0
  138. /package/dist/chunks/{chunk-DOJRPBQU.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
  139. /package/dist/chunks/{chunk-CDQGSZL3.ts.map → chunk-6LD5KX55.ts.map} +0 -0
  140. /package/dist/chunks/{chunk-MDUORMCK.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
  141. /package/dist/chunks/{chunk-STVQQ5KG.ts.map → chunk-7ZIYU5QS.ts.map} +0 -0
  142. /package/dist/chunks/{chunk-AO6EDUA6.ts.map → chunk-AKMV47YC.ts.map} +0 -0
  143. /package/dist/chunks/{chunk-66GAUVDL.ts.map → chunk-BEST45H2.ts.map} +0 -0
  144. /package/dist/chunks/{chunk-73MDIVYJ.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
  145. /package/dist/chunks/{chunk-PHANBTVM.ts.map → chunk-ESKOPQVB.ts.map} +0 -0
  146. /package/dist/chunks/{chunk-MRIPZGPR.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
  147. /package/dist/chunks/{chunk-5WJQ6XOW.ts.map → chunk-GNYGNHFF.ts.map} +0 -0
  148. /package/dist/chunks/{chunk-DFJGTVPX.ts.map → chunk-KYTT7KV4.ts.map} +0 -0
  149. /package/dist/chunks/{chunk-UGSSZWQ6.ts.map → chunk-MKSMJPVA.ts.map} +0 -0
  150. /package/dist/chunks/{chunk-RAMHGOX2.ts.map → chunk-OJARL2TG.ts.map} +0 -0
  151. /package/dist/chunks/{chunk-U77DWNRH.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
  152. /package/dist/chunks/{chunk-QWV3XKNJ.ts.map → chunk-TIMWN2RH.ts.map} +0 -0
  153. /package/dist/chunks/{chunk-Z5O2TLNF.ts.map → chunk-TZC7GDJE.ts.map} +0 -0
  154. /package/dist/chunks/{chunk-UCD7TQDT.ts.map → chunk-U6GXGY5G.ts.map} +0 -0
  155. /package/dist/chunks/{chunk-PWA3YN5Z.ts.map → chunk-XEODG2PE.ts.map} +0 -0
  156. /package/dist/chunks/{chunk-TO2GMX3K.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
  157. /package/dist/chunks/{chunk-JY2RVOUL.ts.map → chunk-Y4UVXOLQ.ts.map} +0 -0
  158. /package/dist/chunks/{chunk-M5IV3KL2.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
  159. /package/dist/chunks/{config-B37W7FXW.ts.map → config-HYG7G5EM.ts.map} +0 -0
  160. /package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
  161. /package/dist/chunks/{conflict-review-BQSHSJB3.ts.map → conflict-review-GXA6Y7KY.ts.map} +0 -0
  162. /package/dist/chunks/{continuous-record-4DQ2EAZS.ts.map → continuous-record-EPHGR2DU.ts.map} +0 -0
  163. /package/dist/chunks/{continuous-sync-ZT3UEQLK.ts.map → continuous-sync-TTOSJCQU.ts.map} +0 -0
  164. /package/dist/chunks/{local-fields-B6JWLPAE.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
  165. /package/dist/chunks/{manager-result-dispatcher-NODX3KKH.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
  166. /package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
  167. /package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
  168. /package/dist/chunks/{setup-switch-66GWMBNH.ts.map → setup-switch-KLGESLQX.ts.map} +0 -0
  169. /package/dist/chunks/{setup-wizard-OHZXJVQZ.ts.map → setup-wizard-ZQAK35BP.ts.map} +0 -0
  170. /package/dist/chunks/{snapshot-VRHNAUUH.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
  171. /package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
  172. /package/dist/chunks/{sync-inspection-4WFQEJOE.ts.map → sync-inspection-MOMLGUTD.ts.map} +0 -0
  173. /package/dist/chunks/{sync-state-SWLSMK4G.ts.map → sync-state-7L4NT7W2.ts.map} +0 -0
@@ -0,0 +1,142 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
4
+
5
+ type MutationGuards = { signal?: AbortSignal; validateMutation?: () => void };
6
+
7
+ function assertCurrent(options: MutationGuards) {
8
+ options.validateMutation?.();
9
+ options.signal?.throwIfAborted();
10
+ }
11
+
12
+ function isMissing(error: unknown) {
13
+ const code = (error as NodeJS.ErrnoException).code;
14
+ return code === "ENOENT" || code === "ENOTDIR";
15
+ }
16
+
17
+ async function targetMissing(target: string, options: MutationGuards) {
18
+ assertCurrent(options);
19
+ try {
20
+ await fs.lstat(target);
21
+ assertCurrent(options);
22
+ return false;
23
+ } catch (error) {
24
+ assertCurrent(options);
25
+ if (!isMissing(error)) throw error;
26
+ return true;
27
+ }
28
+ }
29
+
30
+ async function queueKey(target: string, options: MutationGuards) {
31
+ assertCurrent(options);
32
+ try {
33
+ const key = await fs.realpath(target);
34
+ assertCurrent(options);
35
+ return key;
36
+ } catch (error) {
37
+ assertCurrent(options);
38
+ if (!isMissing(error)) throw error;
39
+ return path.resolve(target);
40
+ }
41
+ }
42
+
43
+ /** Resolve missing descendants too, including descendants of a file scheduled for removal. */
44
+ async function assertCanonicalParent(target: string, options: MutationGuards) {
45
+ const parent = path.dirname(path.resolve(target));
46
+ const suffix: string[] = [];
47
+ for (let current = parent; ; current = path.dirname(current)) {
48
+ assertCurrent(options);
49
+ let canonical: string;
50
+ try {
51
+ canonical = await fs.realpath(current);
52
+ } catch (error) {
53
+ assertCurrent(options);
54
+ if (!isMissing(error) || path.dirname(current) === current) throw error;
55
+ suffix.unshift(path.basename(current));
56
+ continue;
57
+ }
58
+ assertCurrent(options);
59
+ // Pi queues existing paths by realpath and missing paths by lexical resolve.
60
+ // Its public API cannot reserve a missing alias key while that alias exists,
61
+ // or report a key that changes during registration. Refuse aliased parents
62
+ // before mutation rather than risk a non-reentrant queue or overwrite a writer.
63
+ if (path.join(canonical, ...suffix) !== parent)
64
+ throw new Error(
65
+ "Snapshot mutation through an aliased parent is unsafe; use the canonical agent/session path. Transaction evidence retained for review.",
66
+ );
67
+ return;
68
+ }
69
+ }
70
+
71
+ export async function assertCanonicalMutationTargets(targets: readonly string[], options: MutationGuards) {
72
+ for (const target of targets) await assertCanonicalParent(target, options);
73
+ }
74
+
75
+ export async function withTargetQueues<T>(
76
+ targets: readonly string[],
77
+ options: MutationGuards,
78
+ action: (heldKeys: ReadonlySet<string>) => Promise<T>,
79
+ ): Promise<T> {
80
+ // Check the whole group before reserving any key or changing live files.
81
+ await assertCanonicalMutationTargets(targets, options);
82
+ const keys = new Set<string>();
83
+ const missing = new Set<string>();
84
+ for (const target of targets) {
85
+ if (await targetMissing(target, options)) missing.add(target);
86
+ const key = await queueKey(target, options);
87
+ if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
88
+ keys.add(key);
89
+ }
90
+ const ordered = [...keys].sort();
91
+ async function acquire(index: number): Promise<T> {
92
+ assertCurrent(options);
93
+ const key = ordered[index];
94
+ // Pass the resolved key, not an alias whose lookup can select a different key.
95
+ if (key) return withFileMutationQueue(key, () => acquire(index + 1));
96
+ for (const target of targets) {
97
+ await assertCanonicalParent(target, options);
98
+ if (missing.has(target) && !(await targetMissing(target, options)))
99
+ throw new Error("Missing transaction target appeared during queue acquisition; review is required.");
100
+ if (!keys.has(await queueKey(target, options)))
101
+ throw new Error("Transaction queue identity changed; review is required.");
102
+ }
103
+ assertCurrent(options);
104
+ return action(keys);
105
+ }
106
+ return acquire(0);
107
+ }
108
+
109
+ export async function withMissingTargetQueues<T>(
110
+ targets: readonly string[],
111
+ heldKeys: ReadonlySet<string>,
112
+ options: MutationGuards,
113
+ action: (heldKeys: ReadonlySet<string>) => Promise<T>,
114
+ ): Promise<T> {
115
+ const unique = [...new Set(targets)];
116
+ const requireAbsent = async () => {
117
+ for (const target of unique) {
118
+ await assertCanonicalParent(target, options);
119
+ if (!(await targetMissing(target, options)))
120
+ throw new Error("Snapshot target changed before destination queue reservation; evidence retained.");
121
+ }
122
+ };
123
+ // Canonical parents make each missing key equal to the intended installed key.
124
+ // Retain the old physical key and exclude it from these non-reentrant queues.
125
+ const keys = unique
126
+ .map((target) => path.resolve(target))
127
+ .filter((key) => !heldKeys.has(key))
128
+ .sort();
129
+ await requireAbsent();
130
+ const combined = new Set([...heldKeys, ...keys]);
131
+ async function acquire(index: number): Promise<T> {
132
+ assertCurrent(options);
133
+ const key = keys[index];
134
+ if (key) return withFileMutationQueue(key, () => acquire(index + 1));
135
+ // A writer registered in the removal gap must finish before this check.
136
+ // Even recognizable pre/postimages belong to that writer, not this transaction.
137
+ await requireAbsent();
138
+ assertCurrent(options);
139
+ return action(combined);
140
+ }
141
+ return acquire(0);
142
+ }
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import type { Stats } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
6
5
  import { assertWithinRoot, isDeniedPath, isPathInside } from "../paths.js";
7
6
  import { syncDirectory, writeJson } from "../state/json-file.js";
8
7
  import { withLock } from "../state/lock.js";
@@ -11,6 +10,7 @@ import { stateDir } from "../state/state-directory.js";
11
10
  import { mergePathIdentity } from "../sync/file-merge-planner.js";
12
11
  import { agentDir, configuredSessionDir } from "./session-paths.js";
13
12
  import { caseReplacementSpelling, coalesceCaseReplacements, isCaseReplacement } from "./snapshot-case-replacement.js";
13
+ import { withMissingTargetQueues, withTargetQueues } from "./snapshot-mutation-queues.js";
14
14
  import { sessionStorageRoot } from "./snapshot-paths.js";
15
15
  import {
16
16
  prepareSessionRootTransition,
@@ -69,7 +69,7 @@ export async function applySnapshotTransaction(plan: SnapshotApplyPlan, options:
69
69
  const targets = [...new Set([...coalesced.plan.deletes, ...coalesced.plan.writes.map((item) => item.target)])].sort();
70
70
  if (targets.length > 16_384)
71
71
  throw new Error("Snapshot transaction exceeds its target bound; review a smaller transfer.");
72
- return withTargetQueues(targets, async () => {
72
+ return withTargetQueues(targets, options, async (heldKeys) => {
73
73
  options.signal?.throwIfAborted();
74
74
  if (
75
75
  targets.some((target) =>
@@ -134,6 +134,10 @@ export async function applySnapshotTransaction(plan: SnapshotApplyPlan, options:
134
134
  ...(entry.afterTarget ? [[entry.afterTarget, entry] as const] : []),
135
135
  ]),
136
136
  );
137
+ // A released additional key cannot authorize rollback, even for recognized raced bytes.
138
+ const changesQueueKey = transaction.journal.entries.some(
139
+ (entry) => !heldKeys.has(entry.target) || (entry.afterTarget && !heldKeys.has(entry.afterTarget)),
140
+ );
137
141
  try {
138
142
  for (const target of transaction.deletes.sort((a, b) => a.length - b.length)) {
139
143
  options.signal?.throwIfAborted();
@@ -142,75 +146,81 @@ export async function applySnapshotTransaction(plan: SnapshotApplyPlan, options:
142
146
  await removeOwnedTarget(transaction.directory, entry, transaction.journal, options);
143
147
  await syncDirectory(path.dirname(target));
144
148
  }
145
- await withCaseDestinationQueues([...coalesced.replacements], options, async () => {
146
- await retireRemovalIntent(
147
- transaction.directory,
148
- transaction.journal,
149
- transaction.journal.entries.filter(
150
- (entry) => entry.removalPending && (entry.afterImage !== "missing" || (entry.postFiles?.length ?? 0) > 0),
151
- ),
152
- options,
153
- );
154
- // Only this uninterrupted call knows which deleted writes have not attempted installation yet.
155
- const uninstalledWrites = new Set(transaction.deletedWrites);
156
- for (const item of plan.writes) {
157
- options.signal?.throwIfAborted();
158
- const entry = entriesByTarget.get(item.target);
159
- if (!entry) throw new Error("Unowned transaction target.");
160
- const deletedByThisCall = uninstalledWrites.delete(entry.target);
161
- await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
162
- options.validateMutation?.();
163
- options.signal?.throwIfAborted();
164
- await fs.mkdir(path.dirname(item.target), { recursive: true });
165
- options.validateMutation?.();
166
- options.signal?.throwIfAborted();
167
- const temp = path.join(path.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
168
- try {
169
- let mode = 0o600;
170
- try {
171
- const stat = await fs.lstat(item.target);
172
- if (stat.isFile()) mode = stat.mode & 0o777;
173
- } catch (error) {
174
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
175
- }
176
- const handle = await fs.open(temp, "wx", mode);
177
- try {
178
- await handle.writeFile(item.content);
179
- await handle.sync();
180
- } finally {
181
- await handle.close();
182
- }
149
+ await withMissingTargetQueues(
150
+ transaction.journal.entries
151
+ .filter((entry) => entry.removalPending)
152
+ .flatMap((entry) => [entry.target, ...(entry.afterTarget ? [entry.afterTarget] : [])]),
153
+ heldKeys,
154
+ options,
155
+ async () => {
156
+ await retireRemovalIntent(
157
+ transaction.directory,
158
+ transaction.journal,
159
+ transaction.journal.entries.filter(
160
+ (entry) => entry.removalPending && (entry.afterImage !== "missing" || (entry.postFiles?.length ?? 0) > 0),
161
+ ),
162
+ options,
163
+ );
164
+ // Only this uninterrupted call knows which deleted writes have not attempted installation yet.
165
+ const uninstalledWrites = new Set(transaction.deletedWrites);
166
+ for (const item of plan.writes) {
167
+ options.signal?.throwIfAborted();
168
+ const entry = entriesByTarget.get(item.target);
169
+ if (!entry) throw new Error("Unowned transaction target.");
170
+ const deletedByThisCall = uninstalledWrites.delete(entry.target);
183
171
  await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
172
+ options.validateMutation?.();
184
173
  options.signal?.throwIfAborted();
174
+ await fs.mkdir(path.dirname(item.target), { recursive: true });
185
175
  options.validateMutation?.();
186
- await fs.rename(temp, item.target);
187
- await syncDirectory(path.dirname(item.target));
188
- } finally {
189
- await fs.rm(temp, { force: true });
176
+ options.signal?.throwIfAborted();
177
+ const temp = path.join(path.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
178
+ try {
179
+ let mode = 0o600;
180
+ try {
181
+ const stat = await fs.lstat(item.target);
182
+ if (stat.isFile()) mode = stat.mode & 0o777;
183
+ } catch (error) {
184
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
185
+ }
186
+ const handle = await fs.open(temp, "wx", mode);
187
+ try {
188
+ await handle.writeFile(item.content);
189
+ await handle.sync();
190
+ } finally {
191
+ await handle.close();
192
+ }
193
+ await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
194
+ options.signal?.throwIfAborted();
195
+ options.validateMutation?.();
196
+ await fs.rename(temp, item.target);
197
+ await syncDirectory(path.dirname(item.target));
198
+ } finally {
199
+ await fs.rm(temp, { force: true });
200
+ }
190
201
  }
191
- }
192
- options.validateMutation?.();
193
- options.signal?.throwIfAborted();
194
- await syncMutationParents(
195
- targets,
196
- [transaction.journal.root, ...(transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : [])],
197
- options,
198
- );
199
- options.validateMutation?.();
200
- options.signal?.throwIfAborted();
201
- });
202
+ options.validateMutation?.();
203
+ options.signal?.throwIfAborted();
204
+ await syncMutationParents(
205
+ targets,
206
+ [transaction.journal.root, ...(transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : [])],
207
+ options,
208
+ );
209
+ options.validateMutation?.();
210
+ options.signal?.throwIfAborted();
211
+ },
212
+ );
202
213
  } catch (error) {
203
214
  // Aborted owners cannot roll back files after another session has replaced them.
204
215
  if (options.signal?.aborted)
205
216
  throw new Error("Snapshot apply cancelled; guarded transaction evidence retained for review.");
206
- // Additional spelling queues have been released: do not start a case rollback
207
- // under only the original key. Startup recovery reserves its own destination key.
208
- if (coalesced.replacements.size)
209
- throw new Error("Case replacement apply failed; guarded transaction evidence retained for review.", {
217
+ // Additional queues have been released. Recovery must reserve missing keys afresh.
218
+ if (changesQueueKey)
219
+ throw new Error("Snapshot apply failed; guarded transaction evidence retained for review.", {
210
220
  cause: error,
211
221
  });
212
222
  try {
213
- await restoreTransaction(transaction.directory, transaction.journal, options);
223
+ await restoreTransaction(transaction.directory, transaction.journal, options, heldKeys);
214
224
  } catch (recoveryError) {
215
225
  throw new AggregateError(
216
226
  [error, recoveryError],
@@ -285,8 +295,8 @@ export async function recoverPendingSnapshotTransactions(options: TransactionOpt
285
295
  await removeTransaction(directory);
286
296
  continue;
287
297
  }
288
- await withTargetQueues(journal.entries.map((entry) => entry.target).sort(), () =>
289
- restoreTransaction(directory, journal, { ...options, sessionDir }),
298
+ await withTargetQueues(journal.entries.map((entry) => entry.target).sort(), options, (heldKeys) =>
299
+ restoreTransaction(directory, journal, { ...options, sessionDir }, heldKeys),
290
300
  );
291
301
  }
292
302
  }
@@ -388,7 +398,36 @@ async function prepareTransaction(
388
398
  return { directory, journal, deletes: indexed.deletes, deletedWrites: indexed.deletedWrites };
389
399
  }
390
400
 
391
- async function restoreTransaction(directory: string, journal: TransactionJournal, options: TransactionOptions) {
401
+ async function restoreTransaction(
402
+ directory: string,
403
+ journal: TransactionJournal,
404
+ options: TransactionOptions,
405
+ heldKeys: ReadonlySet<string>,
406
+ ) {
407
+ validateJournal(directory, journal, options.sessionDir);
408
+ const missingSpellings: string[] = [];
409
+ for (const entry of journal.entries) {
410
+ if (!entry.afterTarget) continue;
411
+ options.validateMutation?.();
412
+ options.signal?.throwIfAborted();
413
+ const spelling = await caseReplacementSpelling(entry.target, entry.afterTarget);
414
+ options.validateMutation?.();
415
+ options.signal?.throwIfAborted();
416
+ if (spelling === undefined) missingSpellings.push(entry.target, entry.afterTarget);
417
+ }
418
+ // Reserve an interrupted removal's other spelling before staging or image checks:
419
+ // recognizable bytes installed by a writer in that gap are not our postimage.
420
+ return withMissingTargetQueues(missingSpellings, heldKeys, options, (reservedKeys) =>
421
+ restoreQueuedTransaction(directory, journal, options, reservedKeys),
422
+ );
423
+ }
424
+
425
+ async function restoreQueuedTransaction(
426
+ directory: string,
427
+ journal: TransactionJournal,
428
+ options: TransactionOptions,
429
+ heldKeys: ReadonlySet<string>,
430
+ ) {
392
431
  validateJournal(directory, journal, options.sessionDir);
393
432
  options.validateMutation?.();
394
433
  options.signal?.throwIfAborted();
@@ -453,8 +492,13 @@ async function restoreTransaction(directory: string, journal: TransactionJournal
453
492
  !(await removeOwnedTarget(directory, entry, journal, options, before))
454
493
  )
455
494
  continue;
456
- await withCaseDestinationQueues(
457
- needsCaseQueue && entry.afterTarget ? [[entry.afterTarget, entry.target]] : [],
495
+ await withMissingTargetQueues(
496
+ entry.afterTarget && (needsCaseQueue || verified === "missing")
497
+ ? [entry.target, entry.afterTarget]
498
+ : verified.startsWith("directory:")
499
+ ? [entry.target]
500
+ : [],
501
+ heldKeys,
458
502
  options,
459
503
  async () => {
460
504
  const atRename = await verifyTarget(directory, entry, journal);
@@ -501,17 +545,19 @@ async function restoreTransaction(directory: string, journal: TransactionJournal
501
545
  options.validateMutation?.();
502
546
  options.signal?.throwIfAborted();
503
547
  if (!(await removeOwnedTarget(directory, entry, journal, options, before))) continue;
504
- const atRename = await verifyTarget(directory, entry, journal);
505
- options.validateMutation?.();
506
- options.signal?.throwIfAborted();
507
- if (atRename === before) continue;
508
- await retireRestorationIntent(directory, journal, entry, options);
509
- const atInstall = await verifyTarget(directory, entry, journal, atRename === "missing");
510
- options.validateMutation?.();
511
- options.signal?.throwIfAborted();
512
- if (atInstall === before) continue;
513
- await fs.rename(temporary, entry.target);
514
- await syncDirectory(path.dirname(entry.target));
548
+ await withMissingTargetQueues([entry.target], heldKeys, options, async () => {
549
+ const atRename = await verifyTarget(directory, entry, journal);
550
+ options.validateMutation?.();
551
+ options.signal?.throwIfAborted();
552
+ if (atRename === before) return;
553
+ await retireRestorationIntent(directory, journal, entry, options);
554
+ const atInstall = await verifyTarget(directory, entry, journal, atRename === "missing");
555
+ options.validateMutation?.();
556
+ options.signal?.throwIfAborted();
557
+ if (atInstall === before) return;
558
+ await fs.rename(temporary, entry.target);
559
+ await syncDirectory(path.dirname(entry.target));
560
+ });
515
561
  } finally {
516
562
  await fs.rm(temporary, { recursive: true, force: true });
517
563
  }
@@ -867,55 +913,6 @@ async function assertSafeParents(root: string, sessionRoot: string | undefined,
867
913
  }
868
914
  }
869
915
  }
870
- async function withTargetQueues<T>(targets: string[], action: () => Promise<T>) {
871
- const keys = new Set<string>();
872
- for (const target of targets) {
873
- let key = path.resolve(target);
874
- try {
875
- key = await fs.realpath(target);
876
- } catch (error) {
877
- const code = (error as NodeJS.ErrnoException).code;
878
- if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
879
- }
880
- if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
881
- keys.add(key);
882
- }
883
- async function acquire(index: number): Promise<T> {
884
- const target = targets[index];
885
- return target ? withFileMutationQueue(target, () => acquire(index + 1)) : action();
886
- }
887
- return acquire(0);
888
- }
889
- async function withCaseDestinationQueues<T>(
890
- spellings: readonly (readonly [string, string])[],
891
- options: TransactionOptions,
892
- action: () => Promise<T>,
893
- ): Promise<T> {
894
- if (!spellings.length) return action();
895
- const requireAbsent = async () => {
896
- for (const [before, after] of spellings) {
897
- options.validateMutation?.();
898
- options.signal?.throwIfAborted();
899
- const spelling = await caseReplacementSpelling(before, after);
900
- options.validateMutation?.();
901
- options.signal?.throwIfAborted();
902
- if (spelling !== undefined)
903
- throw new Error("Case replacement changed before destination queue reservation; evidence retained.");
904
- }
905
- };
906
- // Existing aliases share one non-reentrant realpath queue. Only after removal can
907
- // we reserve the distinct missing spelling while retaining the original queue.
908
- // Pi writers that registered in the gap finish before this callback; refuse their
909
- // bytes, including recognizable pre/postimages, rather than treating them as ours.
910
- await requireAbsent();
911
- return withTargetQueues(
912
- spellings.map(([, after]) => after),
913
- async () => {
914
- await requireAbsent();
915
- return action();
916
- },
917
- );
918
- }
919
916
  async function completeTransaction(directory: string, journal: TransactionJournal) {
920
917
  // Publish completion before deleting any backup. A surviving journal after power loss
921
918
  // may only retry cleanup, never roll back already accepted files.
@@ -93,7 +93,7 @@ export function buildAgentEvidence(
93
93
  const evidence: AgentEvidence = { units: [], files: [], unsupported: [] };
94
94
  if (plan.kind !== "planned") return evidence;
95
95
  if (plan.conflicts.length > AGENT_MAX_UNITS)
96
- throw new Error("Agent mode paused: too many conflict paths for bounded analysis.");
96
+ throw new Error("Autonomous sync paused: too many conflict paths for bounded analysis.");
97
97
  for (const conflict of plan.conflicts) {
98
98
  try {
99
99
  if (conflict.reason !== "both-changed") throw new Error("unsupported conflict");
@@ -159,7 +159,7 @@ export function buildAgentEvidence(
159
159
  }
160
160
  }
161
161
  if (evidence.units.length > AGENT_MAX_UNITS || Buffer.byteLength(JSON.stringify(evidence.units)) > AGENT_INPUT_BYTES)
162
- throw new Error("Agent mode paused: conflict evidence exceeds its input bound.");
162
+ throw new Error("Autonomous sync paused: conflict evidence exceeds its input bound.");
163
163
  // Scan only the actual disclosure image; skipSecretScan never authorizes model disclosure.
164
164
  if (
165
165
  scanSnapshot({
@@ -169,7 +169,7 @@ export function buildAgentEvidence(
169
169
  ),
170
170
  }).length
171
171
  )
172
- throw new Error("Agent mode paused: possible secrets in model evidence; nothing was disclosed.");
172
+ throw new Error("Autonomous sync paused: possible secrets in model evidence; nothing was disclosed.");
173
173
  return evidence;
174
174
  }
175
175
 
@@ -34,24 +34,27 @@ export async function resolveAgentPlan(
34
34
  return plan;
35
35
  }
36
36
  if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc"))
37
- throw new Error("Agent mode paused: TUI/RPC is required.");
37
+ throw new Error("Autonomous sync paused: TUI/RPC is required.");
38
38
  const ancestors = (await readMergeAncestors(config, state)) ?? [];
39
39
  await validate();
40
40
  let evidence: ReturnType<typeof buildAgentEvidence>;
41
41
  try {
42
42
  evidence = buildAgentEvidence(plan, local, remote, ancestors);
43
43
  } catch {
44
- throw new ContinuousError("unsafe", "Agent mode paused: unsafe or unsupported conflict evidence.");
44
+ throw new ContinuousError("unsafe", "Autonomous sync paused: unsafe or unsupported conflict evidence.");
45
45
  }
46
46
  if (!config.partialSync && evidence.unsupported.length)
47
- throw new ContinuousError("unsafe", "Agent mode paused: unsupported conflicts prevent an all-or-nothing transfer.");
47
+ throw new ContinuousError(
48
+ "unsafe",
49
+ "Autonomous sync paused: unsupported conflicts prevent an all-or-nothing transfer.",
50
+ );
48
51
  if (!evidence.files.length) {
49
52
  if (options.agentRetryRefusal)
50
53
  throw new ContinuousError("unsafe", "No eligible cached refusal for the current evidence. Use Sync now.");
51
54
  if (config.partialSync) return plan;
52
55
  throw new ContinuousError(
53
56
  "unsafe",
54
- "Agent mode paused: unsupported conflict or missing verified ancestor; evidence is unchanged.",
57
+ "Autonomous sync paused: unsupported conflict or missing verified ancestor; evidence is unchanged.",
55
58
  );
56
59
  }
57
60
  if (!evidence.units.length) {
@@ -59,7 +62,7 @@ export async function resolveAgentPlan(
59
62
  throw new ContinuousError("unsafe", "No eligible cached refusal for the current evidence. Use Sync now.");
60
63
  return applyAgentCandidate(plan, evidence, { version: 1, results: [] });
61
64
  }
62
- if (!ctx.model) throw new ContinuousError("model", "Agent mode paused: select an available model first.");
65
+ if (!ctx.model) throw new ContinuousError("model", "Autonomous sync paused: select an available model first.");
63
66
  const model = digest(ctx.model);
64
67
  const key = digest([
65
68
  1,
@@ -75,7 +78,7 @@ export async function resolveAgentPlan(
75
78
  try {
76
79
  return applyAgentCandidate(plan, evidence, options.agentAcceptance.candidate);
77
80
  } catch {
78
- throw new ContinuousError("unsafe", "Agent mode paused: candidate refused or unsupported conflict remains.");
81
+ throw new ContinuousError("unsafe", "Autonomous sync paused: candidate refused or unsupported conflict remains.");
79
82
  }
80
83
  }
81
84
  const validateEvidence = async (signal = options.signal) => {
@@ -85,7 +88,7 @@ export async function resolveAgentPlan(
85
88
  signal?.throwIfAborted();
86
89
  };
87
90
  await check();
88
- if (digest(ctx.model) !== model) throw new Error("Agent mode paused: selected model changed.");
91
+ if (digest(ctx.model) !== model) throw new Error("Autonomous sync paused: selected model changed.");
89
92
  const current = await createSnapshot(config.snapshotIdentity, { ...snapshotOptions, signal });
90
93
  await check();
91
94
  const currentState = await readStateForConfig(config);
@@ -98,7 +101,7 @@ export async function resolveAgentPlan(
98
101
  !currentHead ||
99
102
  !backend.sameRevision(head.revision, currentHead.revision)
100
103
  )
101
- throw new ContinuousError("stale", "Agent mode paused: conflict evidence changed; retry a fresh sync.");
104
+ throw new ContinuousError("stale", "Autonomous sync paused: conflict evidence changed; retry a fresh sync.");
102
105
  };
103
106
  throw new AgentResolutionRequired(key, evidence, validateEvidence);
104
107
  }
@@ -39,9 +39,9 @@ export async function runAgentResolver(
39
39
  beforeRequest?: (signal: AbortSignal) => Promise<void>,
40
40
  ): Promise<string> {
41
41
  const model = ctx.model;
42
- if (!model) throw new ContinuousError("model", "Agent mode paused: select an available model first.");
42
+ if (!model) throw new ContinuousError("model", "Autonomous sync paused: select an available model first.");
43
43
  if (!Number.isSafeInteger(model.maxTokens) || model.maxTokens < 0)
44
- throw new Error("Agent mode paused: selected model has an invalid output limit.");
44
+ throw new Error("Autonomous sync paused: selected model has an invalid output limit.");
45
45
  // Pi uses zero for unknown limits, including virtual models. Its registry caps routed requests.
46
46
  const maxTokens = model.maxTokens > 0 ? Math.min(AGENT_MAX_TOKENS, model.maxTokens) : AGENT_MAX_TOKENS;
47
47
  const controller = new AbortController();
@@ -74,7 +74,7 @@ export async function runAgentResolver(
74
74
  },
75
75
  });
76
76
  const childModel = runtime.getModel(provider, "resolver");
77
- if (!childModel) throw new Error("Agent mode paused: resolver model unavailable.");
77
+ if (!childModel) throw new Error("Autonomous sync paused: resolver model unavailable.");
78
78
  ({ session } = await createAgentSession({
79
79
  cwd: ctx.cwd,
80
80
  model: childModel,
@@ -104,7 +104,7 @@ export async function runAgentResolver(
104
104
  });
105
105
  child.agent.streamFunction = async (_model, context, options) => {
106
106
  ownedSignal.throwIfAborted();
107
- if (++calls > 1) throw new Error("Agent mode paused: one provider request per candidate is permitted.");
107
+ if (++calls > 1) throw new Error("Autonomous sync paused: one provider request per candidate is permitted.");
108
108
  await validate(ownedSignal);
109
109
  ownedSignal.throwIfAborted();
110
110
  if (beforeRequest) {
@@ -146,15 +146,16 @@ export async function runAgentResolver(
146
146
  last.stopReason !== "stop" ||
147
147
  last.content.some((block) => block.type !== "text" && block.type !== "thinking")
148
148
  )
149
- throw new Error("Agent mode paused: incomplete, failed or tool-bearing model output.");
149
+ throw new Error("Autonomous sync paused: incomplete, failed or tool-bearing model output.");
150
150
  const output = last.content
151
151
  .filter((block) => block.type === "text")
152
152
  .map((block) => block.text)
153
153
  .join("");
154
- if (Buffer.byteLength(output) > AGENT_OUTPUT_BYTES) throw new Error("Agent mode paused: output bound exceeded.");
154
+ if (Buffer.byteLength(output) > AGENT_OUTPUT_BYTES)
155
+ throw new Error("Autonomous sync paused: output bound exceeded.");
155
156
  if (ctx.hasUI)
156
157
  ctx.ui.notify(
157
- `Pi Sync agent candidate: ${last.usage.totalTokens} tokens. Semantic correctness is not guaranteed.`,
158
+ `Pi Sync resolver candidate: ${last.usage.totalTokens} tokens. Semantic correctness is not guaranteed.`,
158
159
  "info",
159
160
  );
160
161
  return output;
@@ -169,13 +170,13 @@ export async function runAgentResolver(
169
170
  if (failure.retry)
170
171
  throw new ContinuousError(
171
172
  failure.kind,
172
- "Agent mode paused: transient resolver transport failure.",
173
+ "Autonomous sync paused: transient resolver transport failure.",
173
174
  failure.delay,
174
175
  );
175
176
  // Provider errors can quote submitted content or auth; never surface their payload.
176
177
  throw new ContinuousError(
177
178
  "model",
178
- "Agent mode paused: resolver failed or returned unusable output. Check model access and retry later.",
179
+ "Autonomous sync paused: resolver failed or returned unusable output. Check model access and retry later.",
179
180
  );
180
181
  } finally {
181
182
  clearTimeout(timer);
@@ -92,7 +92,7 @@ export async function withAgentClaim<T>(fn: (assertOwner: () => Promise<void>) =
92
92
  await withClaimGuard(async () => {
93
93
  const existing = await readClaim();
94
94
  if (existing && alive(existing.pid))
95
- throw new ContinuousError("contention", "Agent mode deferred: another local resolver is running.");
95
+ throw new ContinuousError("contention", "Autonomous sync deferred: another local resolver is running.");
96
96
  // A durability failure can occur after rename: cleanup must own partial initialization too.
97
97
  armed = true;
98
98
  await writeJson(paths().claim, claim);
@@ -89,7 +89,7 @@ export async function withAgentSyncLock<T>(
89
89
  try {
90
90
  candidate = parseAgentCandidate(raw, request.evidence);
91
91
  } catch {
92
- throw new ContinuousError("model", "Agent mode paused: invalid resolver output; evidence retained.");
92
+ throw new ContinuousError("model", "Autonomous sync paused: invalid resolver output; evidence retained.");
93
93
  }
94
94
  if (retry && candidate.results.some((row) => "refuse" in row))
95
95
  throw new ContinuousError("model", "The model refused again. The previous refusal was retained.");
@@ -132,7 +132,7 @@ export async function withAgentSyncLock<T>(
132
132
  request = error;
133
133
  }
134
134
  }
135
- throw new ContinuousError("stale", "Agent mode paused: content changed repeatedly; retry later.");
135
+ throw new ContinuousError("stale", "Autonomous sync paused: content changed repeatedly; retry later.");
136
136
  } finally {
137
137
  options.signal?.removeEventListener("abort", onAbort);
138
138
  if (!options.signal?.aborted) clearStatus();