@narumitw/pi-sync 0.54.2 → 0.56.0

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 (194) hide show
  1. package/README.md +40 -15
  2. package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
  3. package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
  4. package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
  5. package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
  6. package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
  7. package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
  8. package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
  9. package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
  10. package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
  11. package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
  12. package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
  13. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
  14. package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
  15. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
  16. package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
  17. package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
  18. package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
  19. package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
  20. package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
  21. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
  22. package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
  23. package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
  24. package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
  25. package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
  26. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
  27. package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
  28. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
  29. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
  30. package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
  31. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
  32. package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
  33. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
  34. package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
  35. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
  36. package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
  37. package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
  38. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
  39. package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
  40. package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
  41. package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
  42. package/dist/chunks/chunk-QGK445K7.ts +21 -0
  43. package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
  44. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
  45. package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
  46. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
  47. package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
  48. package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
  49. package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
  50. package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
  51. package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
  52. package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
  53. package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
  54. package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
  55. package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
  56. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
  57. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  58. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  59. package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
  60. package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
  61. package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
  62. package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
  63. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
  64. package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
  65. package/dist/chunks/config-KGIYTTXQ.ts +37 -0
  66. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
  67. package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
  68. package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
  69. package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
  70. package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
  71. package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
  72. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
  73. package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
  74. package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
  75. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
  76. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
  77. package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
  78. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
  79. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
  80. package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
  81. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
  82. package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
  83. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
  84. package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
  85. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
  86. package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
  87. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
  88. package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
  89. package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
  90. package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
  91. package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
  92. package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
  93. package/dist/index.ts +246 -131
  94. package/dist/index.ts.map +3 -3
  95. package/docs/agent-mode-verification.md +99 -0
  96. package/docs/continuous-sync-verification.md +87 -0
  97. package/docs/settings.md +131 -11
  98. package/docs/tui-contract.md +61 -0
  99. package/package.json +3 -2
  100. package/src/backends/git/git-backend.ts +1 -4
  101. package/src/backends/git/git-runner.ts +1 -5
  102. package/src/backends/s3/s3-backend.ts +3 -25
  103. package/src/backends/s3/s3-client.ts +14 -2
  104. package/src/backends/snapshot-pointer.ts +27 -0
  105. package/src/backends/webdav/webdav-backend.ts +3 -29
  106. package/src/backends/webdav/webdav-client.ts +11 -7
  107. package/src/commands/command-execution.ts +7 -3
  108. package/src/commands/command-handler.ts +15 -3
  109. package/src/commands/command-types.ts +10 -0
  110. package/src/commands/command.ts +4 -1
  111. package/src/settings/config.ts +20 -10
  112. package/src/settings/settings-store.ts +1 -34
  113. package/src/settings/settings-types.ts +10 -0
  114. package/src/settings/settings-validation.ts +4 -1
  115. package/src/snapshot/snapshot-codec.ts +1 -5
  116. package/src/state/lock.ts +13 -10
  117. package/src/state/merge-baseline-store.ts +19 -1
  118. package/src/state/state-directory.ts +8 -1
  119. package/src/sync/agent-candidate.ts +251 -0
  120. package/src/sync/agent-plan.ts +87 -0
  121. package/src/sync/agent-policy.ts +13 -0
  122. package/src/sync/agent-request.ts +17 -0
  123. package/src/sync/agent-runner.ts +188 -0
  124. package/src/sync/agent-store.ts +151 -0
  125. package/src/sync/agent-sync.ts +91 -0
  126. package/src/sync/apply-review.ts +63 -0
  127. package/src/sync/automatic-sync.ts +46 -33
  128. package/src/sync/continuous-errors.ts +85 -0
  129. package/src/sync/continuous-lease.ts +221 -0
  130. package/src/sync/continuous-record.ts +179 -0
  131. package/src/sync/continuous-storage.ts +55 -0
  132. package/src/sync/continuous-sync.ts +516 -0
  133. package/src/sync/continuous-watch.ts +59 -0
  134. package/src/sync/merged-sync.ts +268 -112
  135. package/src/sync/remote-snapshot.ts +2 -3
  136. package/src/sync/settings-merge.ts +19 -3
  137. package/src/sync/setup-switch.ts +1 -5
  138. package/src/sync/startup-check.ts +51 -29
  139. package/src/sync/startup-timings.ts +90 -0
  140. package/src/sync/sync-inspection.ts +16 -5
  141. package/src/sync/sync-mutations.ts +55 -28
  142. package/src/sync/sync-queries.ts +19 -3
  143. package/src/sync/text-merge.ts +3 -3
  144. package/src/sync-extension.ts +75 -19
  145. package/src/ui/agent-mode-setting.ts +91 -0
  146. package/src/ui/manager-state.ts +12 -6
  147. package/src/ui/manager-ui.ts +57 -7
  148. package/src/ui/merge-review.ts +3 -3
  149. package/src/ui/merged-sync-summary.ts +2 -18
  150. package/src/ui/remote-selection-ui.ts +5 -5
  151. package/src/ui/settings-frame.ts +55 -0
  152. package/src/ui/settings-ui.ts +223 -43
  153. package/src/ui/setup/s3-credentials-ui.ts +1 -5
  154. package/src/ui/setup/setup-name-ui.ts +2 -5
  155. package/src/ui/setup/setup-prompts.ts +4 -3
  156. package/src/ui/setup/setup-switcher.ts +2 -0
  157. package/src/ui/sync-behavior.ts +15 -0
  158. package/src/ui/sync-format.ts +69 -38
  159. package/src/ui/sync-setups-ui.ts +3 -5
  160. package/src/ui/transfer-changes.ts +33 -0
  161. package/src/ui/transfer-review.ts +27 -0
  162. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  163. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  164. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  165. package/dist/chunks/chunk-5ZWQ33XL.ts.map +0 -7
  166. package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
  167. package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
  168. package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
  169. package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
  170. package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
  171. package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
  172. package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
  173. package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
  174. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  175. package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
  176. package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
  177. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  178. package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
  179. package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
  180. package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
  181. package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
  182. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
  183. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
  184. /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
  185. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
  186. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
  187. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
  188. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
  189. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
  190. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
  191. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
  192. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
  193. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
  194. /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
package/docs/settings.md CHANGED
@@ -6,6 +6,8 @@
6
6
  - [Required backend shapes](#required-backend-shapes)
7
7
  - [Status display](#status-display)
8
8
  - [Secret scanning](#secret-scanning)
9
+ - [Experimental Agent mode](#experimental-agent-mode)
10
+ - [Experimental Continuous sync](#experimental-continuous-sync)
9
11
  - [Included content](#included-content)
10
12
  - [Unsupported old settings and recovery](#unsupported-old-settings-and-recovery)
11
13
 
@@ -31,9 +33,11 @@ If both paths exist, `pi-sync.json` wins and the legacy file remains untouched.
31
33
 
32
34
  ### Editing in the manager
33
35
 
34
- Open `/sync` → **Settings** for one searchable list ordered by content, destination, automation, merge, and global preferences; these are task groups, not submenus. **Included content** and **Compare synced content** are adjacent. **Storage location** edits the current setup's coordinates, while **Manage sync setups** and **Manage storage connections** retain the catalog and affected-setup reviews. Returning from catalog management refreshes the rows; switching or removing the owning setup closes its old Settings screen.
36
+ Open `/sync` → **Settings** and choose **Sync content**, **Sync behavior**, **Storage & setups**, or **Advanced**. **All settings (search)** provides cross-category typing-to-search over the same rows. **Included paths** and **Compare included paths** are adjacent in Sync content; the latter compares path policies, not file contents. **Storage location** edits the current setup's coordinates, while **Manage sync setups** and **Manage storage connections** retain the catalog and affected-setup reviews. Returning from catalog management refreshes the rows; switching or removing the owning setup closes its old Settings screen.
35
37
 
36
- Simple choices save immediately, preserve the selected row and search during refresh, and restore their previous value if saving fails. Escape closes the screen without undoing successful saves. Complex values retain their input/review flows. This navigation change does not alter schema, defaults, automatic-sync behavior, or experimental opt-ins.
38
+ Descriptions identify the owner: this setup, all setups on this device, or shared connections. Simple choices report Saving and the durable Saved or failed/rejected result, preserve the selected row and search during refresh, and restore their previous value if saving fails. Escape closes the screen without undoing successful saves. Complex values retain their input/review flows. This navigation change does not alter schema, defaults, automatic-sync behavior, or experimental opt-ins.
39
+
40
+ The manager separates current state from transfer, inspection, and management actions. **Check remote status** and **View differences** invoke the existing read/check routes only when selected. Last applied shows a compact snapshot ID with application time explicitly unavailable; full IDs remain in History. It never uses snapshot creation time as application time. See the [TUI behavior contract](tui-contract.md) for scopes and transfer branches.
37
41
 
38
42
  RPC reports the manual settings path and offers the existing setup/connection catalog dialogs, not the TUI preference or masked-credential editors. Print/JSON do not support `/sync`; use the private settings file directly. Main-menu summaries remain cached observations; `/sync status`, `/sync diff`, and `/sync help` remain available through the visible command hints.
39
43
 
@@ -132,7 +136,7 @@ This check does not contact the server or discover setups configured only on oth
132
136
 
133
137
  Every setup requires `sync.include` and explicit `sync.automatic`. Setup asks for automatic sync with Off first; existing values are not rewritten. When enabled, the current setup receives one background startup check in TUI/RPC, including reload/new/resume/fork. Startup no longer performs automatic transfers or opens dialogs; review changes through `/sync`. Print/JSON skip startup checks. Checks compare local hashes and remote metadata against the last sync baseline, so revision changes and missing baselines require review rather than proving a file conflict.
134
138
 
135
- The setting still permits automatic shutdown pushes of selected content when `sessions` is included, in all modes; this is not limited to uploading session files. Reload skips shutdown push. Settings changes apply to subsequent operations; changing this setting does not schedule another startup check until the next session start. Foreground `/sync` cancels a pending check before opening Settings. The check deadline is 30 seconds plus bounded cleanup, with manual retry through `/sync status`; there is no polling. See [Background startup checks](../README.md#background-startup-checks) for recovery and Git cache exceptions.
139
+ The setting still permits automatic shutdown pushes of selected content when `sessions` is included, in all modes; this is not limited to uploading session files. Reload skips shutdown push. Settings changes apply to subsequent operations; changing this setting does not schedule another startup check until the next session start. Foreground `/sync` cancels a pending check before opening Settings. The check deadline is 30 seconds plus bounded cleanup, with manual retry through `/sync status`; this one-shot policy has no polling. Continuous sync is a separate opt-in. See [Background startup checks](../README.md#background-startup-checks) for recovery and Git cache exceptions.
136
140
 
137
141
  Recommended includes the nine non-session Pi roots listed below; Minimal includes `settings.json` and `AGENTS.md`. The save review lists every selected path and the full backend destination. R2/S3 requires an explicitly entered existing bucket and never creates one. Saving setup or connection settings does not contact remote storage.
138
142
 
@@ -144,7 +148,7 @@ A referenced connection cannot be removed.
144
148
  The current setup must be switched before removal when other setups exist. Removing the last setup is allowed; local removal never deletes remote history.
145
149
  Two setups cannot resolve to the same normalized backend location.
146
150
 
147
- The global **After switching setup (all setups)** setting persists as `onSwitch` and applies whenever any setup is made current. It accepts:
151
+ The global **On setup switch** setting persists as `onSwitch` and applies whenever any setup is made current. It accepts:
148
152
 
149
153
  - `ask-before-pull` — switch, then ask in TUI whether to start a reviewed pull;
150
154
  - `pull-after-switch` — require observable UI and start the normal reviewed pull;
@@ -153,14 +157,37 @@ The global **After switching setup (all setups)** setting persists as `onSwitch`
153
157
  ### Status display
154
158
 
155
159
  The global `showStatus` setting accepts a boolean and defaults to `true` when omitted from an existing version 3 document.
156
- Set it through **/sync → Settings → Show status (all setups)**; changes are saved and applied immediately for every setup.
160
+ Set it through **/sync → Settings → Advanced → Show sync status**; changes are saved and applied immediately for every setup.
157
161
  When `false`, pi-sync clears and suppresses status text for background checks, manual and automatic transfers, and review attention.
158
162
  TUI review widgets and TUI/RPC notifications remain available, and lifecycle cleanup still clears any stale status owned by pi-sync.
159
163
 
164
+ ### Debug startup timings
165
+
166
+ The top-level user setting `debug` accepts only `true` or `false` and defaults to `false`.
167
+ Edit `<getAgentDir()>/pi-sync.json` (normally `~/.pi/agent/pi-sync.json`) or use **/sync → Settings → Advanced → Debug startup timings**.
168
+ UI changes save immediately through the existing ordered, atomic settings writer; both manual and UI changes take effect on the next session start, including `/reload`, new, resume, and fork.
169
+ There is no project or environment override.
170
+
171
+ With `debug: true`, TUI/RPC notifications report local initialization after recovery and settings resolution, then background work when it ends.
172
+ Stages include transaction recovery, settings reads, inspection import, backend initialization, local snapshot, sync state read, remote head, operations import, and automatic transfer when those stages run.
173
+ Automatic-transfer details separate authorization, journal recovery, state and local reads, remote-head reads, snapshot downloads, merge planning, backup, publication, local apply, baseline writes, and cleanup.
174
+ Repeated stages aggregate elapsed time and show a call count. Stage output stays bounded during retries.
175
+ Inspection-and-guards, automatic transfer, journal recovery, and unchanged verification include their nested stages; durations overlap and must not be summed.
176
+ An eligible no-change transfer reports `transfer result: unchanged (fast path)`.
177
+ Totals use a monotonic clock and include waits within each flow, not Pi's complete startup, the initial extension import/factory, or time queued for idle startup transfer.
178
+ Recovery failures before valid settings can be read cannot enable a timing report.
179
+
180
+ Interrupted stages are marked `interrupted`; cancelled or failed background work is marked `stopped`.
181
+ Session replacement and shutdown suppress late reports from the old session.
182
+ Labels and durations contain no credentials, paths, file contents, remote addresses, or error text.
183
+ No log file or model message is created. Print/JSON produce no timing notifications.
184
+ The setting does not enable automatic sync or transfer, change safety checks, or measure manual/shutdown operations.
185
+ Set `debug` to `false` or remove it, then start a new session to disable reports.
186
+
160
187
  ### Secret scanning
161
188
 
162
189
  The global `skipSecretScan` setting accepts a boolean and defaults to `false`, including when omitted from an existing version 3 document.
163
- Set it through **/sync → Settings → Skip secret scan (all setups)**; changes are saved immediately and apply to subsequent pushes for every sync setup, including automatic pushes.
190
+ Set it through **/sync → Settings → Advanced → Skip secret scan**; changes are saved immediately and apply to subsequent pushes for every sync setup, including automatic pushes.
164
191
  When `true`, pushes skip the local secret scan; other safety checks and confirmations remain unchanged.
165
192
  Enable it only after reviewing the destination and selected content because files containing secrets can be uploaded.
166
193
  `/sync doctor` still scans and reports possible secrets regardless of this setting.
@@ -194,7 +221,7 @@ RPC remains a read-only summary with the manual `sync.include` path.
194
221
 
195
222
  Every new snapshot stores the normalized included-content selection separately from the files that happened to exist.
196
223
  This preserves selected-but-missing paths without syncing `pi-sync.json`, storage credentials, automatic-sync preferences, or setup names.
197
- **Settings → Compare synced content** opens the same review-first flow as a manager operation when local and remote lists differ.
224
+ **Settings → Sync content → Compare included paths** opens the same review-first flow as a manager operation when local and remote lists differ.
198
225
  Adoption revalidates the remote head, immutable snapshot, reviewed storage coordinates, and local include list before one atomic settings update.
199
226
  The saved state changes only `sync.include`, preserves unknown settings fields, and never pulls files or writes sync state.
200
227
  Its explicit Continue action starts a fresh **Sync now** route, while **Done** leaves the reviewed settings change saved without implying a file operation.
@@ -212,11 +239,104 @@ Pull protects the currently open session file; restart Pi or resume a pulled ses
212
239
 
213
240
  ### Automatic startup transfer
214
241
 
215
- `sync.automaticTransfer` is optional and accepts only a boolean; absent means `false`. Set it through **/sync → Settings → Automatic transfer at startup**. It is separate from `sync.automatic`: the legacy setting retains observation and selected-content shutdown behavior.
242
+ `sync.automaticTransfer` is optional and accepts only a boolean; absent means `false`. Set it through **/sync → Settings → Sync behavior → Startup transfer**. It is separate from `sync.automatic`: the legacy setting retains observation and selected-content shutdown behavior.
243
+
244
+ When explicitly enabled, one startup transfer waits for an idle boundary in TUI/RPC. It requires an accepted baseline and a matching authoritative remote include list, retains all-or-nothing conflict review, checks secrets unless the global scan override is enabled, and never reloads resources. Git, verified conditional WebDAV, and R2/S3 with verified conditional writes support this policy. A saved On preference is not proof of eligibility: baseline, remote policy, idle ownership, conflicts, secrets, and publication capability are checked again at execution. Print/JSON skip automatic startup transfer. A settings save takes effect for subsequent authorization checks; enabling schedules the next session start, while foreground Settings cancels and drains any current work before saving. There is no polling.
245
+
246
+ #### Unchanged transfer fast path
247
+
248
+ After pending merge journals are handled, automatic transfer reads a fresh remote head instead of trusting the earlier startup observation.
249
+ It can return without downloading the remote snapshot or writing a baseline only when the head ID/revision and ordered selection match a complete clean merged baseline, local portable hashes match that baseline, and the current ancestor cache is valid and complete for the enabled merge policy.
250
+ Local files, state, settings, idle authorization, and session ownership are rechecked after ancestor reads.
251
+ This path makes no managed-file, remote, baseline, or ancestor-cache changes; it does not re-download and verify an already accepted immutable snapshot or run normal no-transfer retention cleanup.
252
+
253
+ Manual sync retains the full path. Directional/legacy baselines, missing revisions or selection metadata, unresolved groups, changed policy or content, and missing/invalid/incomplete ancestors also retain the full path.
254
+ Partial-sync setups keep the full path even with no unresolved groups, so snapshot-format upgrades and partial-state maintenance are not skipped.
255
+ A baseline established by a directional push/pull may therefore need one full automatic sync before later starts use the fast path.
256
+ Across startup inspection and transfer, an eligible unchanged setup makes two fresh head reads, with no snapshot download; a normal full no-transfer merge makes three head reads and one snapshot download in addition to inspection.
257
+
258
+ Turning **Startup transfer** off does not revert transfers or remove backups/journals. For interrupted operations, follow [merge recovery](merge-implementation-audit.md). Unknown fields remain preserved and malformed/invalid files block saves, just as for the existing policy.
259
+
260
+ ### Experimental Agent mode
261
+
262
+ `sync.agentMode` is an optional per-setup boolean, default `false`, in settings versions 3, 4, and 5. Open **Settings → Sync behavior → Agent mode (experimental)** and review the setup, destination, selected paths, model disclosure, costs, and autonomous writes before enabling. RPC users can explicitly set the same field in the private settings file. Setting it to `true` is authorization, not just a display preference.
263
+
264
+ Agent mode authorizes one eligible merged sync at the next session start and the same autonomous flow from **Sync now** or `/sync sync`. It can publish and replace/delete selected content without another transfer confirmation. It is independent of `sync.automatic` and `sync.automaticTransfer`; neither existing preference is changed. Disabling Startup transfer alone does not disable Agent mode. Saving never starts a transfer from Settings. The atomic write cannot be cancelled after its final cancellation check. A completed save remains in effect after cancellation or session replacement; reopen Settings to read the saved value. TUI/RPC are supported; print/JSON and shutdown do not start a resolver. Agent mode alone adds no polling or daemon. Separately enabled Continuous sync provides recurring work while Pi remains open.
265
+
266
+ An accepted baseline, matching included-content/local-field policies, and conditional or lease-protected publication remain required. First-source selection, policy migrations, explicit push/pull, and rollback keep their existing reviewed flows. `--yes` does not enable Agent mode or authorize model disclosure. Completed transfers never reload resources automatically.
267
+
268
+ The resolver uses the initiating session's selected model through Pi's public provider/authentication registry. It has an independent in-memory SDK conversation, no tools, no parent conversation, and no discovered extensions, skills, project instructions, or MCP servers. Only conflict units are disclosed. Settings units contain only the conflicting root fields, with coupled fields kept together; the model selects a complete local or remote unit. Other fields are assembled by deterministic code. For text, the model can replace only overlapping edit hunks; code preserves the other bytes and independent edits. Parent model prompts and tools are not changed. This is configuration isolation, not an OS sandbox or proof of semantic correctness.
269
+
270
+ Eligible text is global `AGENTS.md` and `.md`/`.txt` files under `prompts/` or `skills/`. Global `settings.json` uses the existing strict JSON, atomic-value, numeric, coupled-field, unknown-field, and machine-local-field policies. Code/scripts, keybindings, binaries, unknown formats, session histories, deletion/modification conflicts, path collisions, and session-root transitions are not model-resolved. Existing deterministic merges still follow their own explicit settings. Unsupported or refused groups remain unresolved; only an explicitly enabled `partialSync` policy permits progress around them. Preserving versions does not mean a conflict is resolved.
271
+
272
+ A verified accepted ancestor is required. Enabling Agent mode cannot recover historical ancestor bytes that were never retained. If the original accepted bytes are still available on both sides, a conflict-free sync can seed the text ancestors for future runs; otherwise use the existing reviewed recovery flow rather than inventing a base from current edits.
273
+
274
+ Bounds are 8 KiB per input file, 16 conflict paths/units, 24 KiB of serialized model evidence, 16 KiB of candidate output, 8,192 maximum output tokens (or the selected model's smaller positive output limit), and one provider request per candidate. An unknown model limit of zero retains the resolver cap; invalid limits pause before disclosure. Each resolver has a 20-second deadline; the existing overall startup deadline is 30 seconds. At most two candidate requests are made during one triggered flow when remote commit competition changes the plan. Provider retries are disabled. Invalid output, detected stale evidence, missing model/auth, or recovery uncertainty pauses the flow rather than forcing acceptance. These limits are not an exact currency spending cap; in-flight disclosure or charges cannot be undone by cancellation.
275
+
276
+ The existing possible-secret scanner always checks model evidence, even when `skipSecretScan` permits uploads without scanning. It is a heuristic, not a guarantee that all secrets are detected. Do not enable Agent mode for content you cannot disclose to the selected provider. Provider errors and model reasoning are not copied into status or persistent records.
277
+
278
+ Pi sessions sharing one agent directory use one local resolver claim. Other sessions can report deferred without another model request. Inference does not hold the normal sync operation lock; commit rechecks settings, claim ownership, model identity, local bytes, baseline and remote revision. Foreground work, replacement, reload and shutdown cancel/drain the owning task. Another process rechecks authorization before requests and commit; disabling in that process cannot recall an already submitted request. A dead owner's claim can be replaced on a later trigger; unreadable ownership is retained for review. No live claim is removed just because it is old. Claim creation and cleanup share a short-lived cross-process guard. Cleanup does not need the normal operation lock, so another long-running transfer does not retain a completed resolver's claim.
279
+
280
+ Private claim and content-keyed candidate records reside under `<state-root>/agent-resolver/`, outside snapshots, with private permissions. Candidates can be reused only for matching evidence, policies and model identity, and are validated again. They can contain merged text; retain them as private data. Different candidates are retained without automatic retention pruning. Duplicate provider billing remains possible at crash boundaries. Corrupt or unsafe records block reuse rather than being overwritten blindly.
281
+
282
+ Pi-sync's cross-process operation lock excludes other pi-sync transfers, not arbitrary writers. Pi file mutation queues coordinate participating writers only within one process. Checks near rename reduce stale writes but do not provide an atomic compare-and-swap against another Pi process or editor. Session JSONL is not rewritten by the model; other active session writers remain subject to the existing protection limitations. Do not use Agent mode as a guarantee of lossless live editing.
283
+
284
+ Turn Agent mode Off to prevent subsequent requests and invalidate pending authorization. Keep backups, snapshots, conflicts and pending journals; disabling does not undo completed work or erase history. Unknown publication outcomes use the existing journal reconciliation and never authorize blind republishing. Pending journals can be reconciled on a read/check/write/verify backend because recovery does not publish. New autonomous transfers still require conditional or lease-protected publication; turn Agent mode Off before a reviewed manual transfer on a weaker backend. If recovery is blocked, retain evidence and use the existing reviewed recovery routes. Finish/reconcile pending merge journals before a downgrade. No Pi core changes or other extension are required.
285
+
286
+ ### Experimental Continuous sync
287
+
288
+ `sync.continuous` is an optional per-setup boolean, default `false`, in settings versions 3, 4 and 5. Open **Settings → Sync behavior → Continuous sync (experimental)** after enabling Agent mode. RPC users can set the field explicitly in the private settings file. Existing Agent mode consent does not enable recurring requests.
289
+
290
+ The separate review names the setup, storage connection, destination, included paths, recurring costs and automatic writes. An elected idle session uses its selected Pi model; a different session can use a different provider/model. The save uses the existing atomic settings writer. Cancellation after the final publication check cannot undo a committed save. Reopen Settings to read the saved value.
291
+
292
+ #### Activation and ownership
293
+
294
+ Monitoring starts at an eligible TUI/RPC session startup, after awaited local transaction recovery, or after a foreground `/sync` command or manager closes. It replaces the duplicate one-shot startup transfer, not the legacy shutdown policy. Turning Continuous sync off stops its work without disabling Agent mode. Turning Agent mode off also stops monitoring; a saved Continuous sync On value remains ineffective until Agent mode is enabled again.
295
+
296
+ A control-file change during monitoring is detected by a watcher or the five-second local control check. If monitoring is already off, external enablement requires the next session start or a foreground `/sync` command. Print/JSON create no continuous resources. No daemon remains after Pi exits.
297
+
298
+ Upgrade all Pi processes sharing an agent directory before enabling Continuous sync. Older releases do not implement its election or automatic accounting. One scheduler lease coordinates participating sessions/processes sharing the same canonical agent directory. Standbys only check local eligibility/ownership; they do not duplicate remote polling, watchers or inference. Each computer has a separate scheduler. Remote conditional publication and revision checks remain authoritative across computers. The lease does not exclude arbitrary editors or writers in other Pi processes.
299
+
300
+ Agent work, foreground commands, replacement, reload and shutdown cancel and drain owned work before handoff. Idle work resumes through `agent_settled` or a later eligible check. A busy session does not retain leadership indefinitely. Completed atomic writes or remote commits remain committed even when cancellation occurs later.
301
+
302
+ #### Detection, retries and safety blocks
303
+
304
+ Local hints wait for one quiet second. Remote and local snapshots are checked every 60–66 seconds. Missing roots, atomic replacement, unsupported recursive watching and watcher errors retain periodic fallback. Only selected content and control settings generate hints; private caches, backups and scheduler state are excluded.
305
+
306
+ Local changes and retries can cause extra remote requests between polls. Cycles do not overlap. Each cycle has a 30-second deadline plus underlying cleanup. Changes during a cycle coalesce into one later fresh inspection rather than a queue of stale commands.
307
+
308
+ Known transient network/server errors and rate limits retry from five seconds with exponential backoff, bounded jitter and a five-minute cap. Valid server Retry-After delays are capped at five minutes. Backoff survives restart and is reset by successful work or relevant configuration changes, not every file event.
309
+
310
+ Authentication, permissions, disk errors, invalid settings, unsafe evidence and unknown errors stop automatic transfer. Relevant repair, changed evidence or an explicit Sync now action can permit a fresh attempt. Corrupt private records are retained, not replaced with defaults. Local prerequisite checks can detect a repair without network/model work.
311
+
312
+ Blocked conflicts are inspected for changed evidence, but unchanged refusal/invalid output does not spend another model request. Opaque Pi assistant errors and Git failures without structured transport evidence are not classified from text. Select an available model, repair access, or use the existing review routes as needed.
313
+
314
+ Missing baselines, changed selection/local-field rules, unsupported conflicts and weak publication capability remain safety blocks. Code, session histories, deletion conflicts and path collisions are not model-resolved. Existing explicit partial-sync policy alone permits independent progress. No background confirmation dialog or automatic resource reload is added.
315
+
316
+ #### Request limits and private records
317
+
318
+ The existing per-flow candidate limit, 20-second resolver deadline, input/output bounds, tool/resource isolation and mandatory disclosure scan still apply. Cached candidates must pass current evidence checks before reuse.
319
+
320
+ Before each automatic provider dispatch, Pi Sync reserves a request in private durable state. Timeouts, cancellation after reservation and uncertain dispatch outcomes consume that reservation. Automatic requests are limited to:
321
+
322
+ - Two for the same candidate evidence/model contract, across process restart and ownership handoff.
323
+ - Two per replica binding in a rolling 15-minute window, including requests for different candidate keys.
324
+
325
+ A refusal or invalid output blocks unchanged evidence rather than requesting another completion each poll. When the rolling window is full, Status shows the next eligible attempt. Checks, conflict-free transfers and journal recovery can continue without another model request. When the candidate's two attempts are spent, use changed evidence or an explicit retry. The explicit **Sync now** / `/sync sync` path retains its existing per-flow limits; it does not erase automatic accounting. Opening Status never reserves requests or clears limits.
326
+
327
+ These limits are not a currency spending cap or an exactly-once billing guarantee. Candidate accounting has a 128 KiB storage bound. A full record blocks work instead of pruning counts and silently granting more requests.
328
+
329
+ Private records under `<state-root>/continuous/` hold versioned owner/binding metadata, coalesced intent, hashes, safe error categories, timestamps and request counts. They contain no raw credentials, provider errors, reasoning or duplicate conflict text. The binding uses the canonical agent root and setup/destination identity; state-directory migration does not reset accounting. POSIX records are private and use temporary-file-plus-rename publication. Keep the directory as private evidence; do not delete it to repair a transfer.
330
+
331
+ #### Status, recovery and downgrade
332
+
333
+ The existing status key shows waiting, checking/syncing, scheduled retry, another local owner, or a stable blocked reason. **Check remote status** and the manager show advisory ownership, the last successful background check/transfer and safe next actions. Notifications are deduplicated by error/evidence/configuration. Status may itself need network access for its existing remote check, but does not call the model.
334
+
335
+ Pending journals are reconciled before new work, including a newly uncertain publication. Recovery never blindly republishes. An unchanged recovery block is not repeatedly applied. Preserve journals, backups and unresolved versions when proof is unavailable.
216
336
 
217
- When explicitly enabled, one startup transfer waits for an idle boundary in TUI/RPC. It requires an accepted baseline and a matching authoritative remote include list, retains all-or-nothing conflict review, checks secrets unless the global scan override is enabled, and never reloads resources. Git and verified conditional WebDAV support this automatic policy; R2/S3 require manual sync. Print/JSON skip automatic startup transfer. A settings save takes effect for subsequent authorization checks; enabling schedules the next session start, while foreground Settings cancels and drains any current work before saving. There is no polling.
337
+ Turn Continuous sync off and drain its owner before downgrade. Reconcile pending transfers with the current release. Existing Agent mode and shutdown preferences remain separately effective. Private scheduler records are separate from baseline/journal formats and remain retained; no automatic filesystem rollback or state migration is performed.
218
338
 
219
- Turning it off does not revert transfers or remove backups/journals. For interrupted operations, follow [merge recovery](merge-implementation-audit.md). Unknown fields remain preserved and malformed/invalid files block saves, just as for the existing policy.
339
+ For implementation evidence and test scope, see [Continuous sync verification](continuous-sync-verification.md).
220
340
 
221
341
  ### Settings field merge and machine-local fields
222
342
 
@@ -232,7 +352,7 @@ Accepted ancestors are private, hash/identity-bound, bounded records below the d
232
352
 
233
353
  ### Content merge and partial progress
234
354
 
235
- `sync.mergeContent` and `sync.partialSync` are independent experimental booleans, both default `false`. The owning **Content / partial sync** setting offers Off, Content only, Partial only, and Content & partial and confirms an explicit settings-version-5 upgrade. Manual configuration must also declare version 5. Settings version 5 retains the portable-policy behavior of version 4; moving from a legacy setup can require the existing separately confirmed directional policy migration. Disabling options retains the schema and recovery evidence.
355
+ `sync.mergeContent` and `sync.partialSync` are independent experimental booleans, both default `false`. The owning **Advanced → Content merge / partial progress** setting offers Off, Merge only, Merge + partial, and Partial only and confirms an explicit settings-version-5 upgrade. Manual configuration must also declare version 5. Settings version 5 retains the portable-policy behavior of version 4; moving from a legacy setup can require the existing separately confirmed directional policy migration. Disabling options retains the schema and recovery evidence.
236
356
 
237
357
  Content merge initially supports global `AGENTS.md` and `.md`/`.txt` prompt/skill resources. Strict UTF-8, 1 MiB/file and a four-million-cell complexity bound apply. BOM, CRLF/LF and final-newline bytes are not silently normalized; unsupported/binary/oversized input remains a file conflict. Output never includes conflict markers. JSON/keybindings/executable resources are not treated as generic text; settings has its separate strategy. A clean line merge is not proof of semantic correctness.
238
358
 
@@ -0,0 +1,61 @@
1
+ # Sync TUI behavior contract
2
+
3
+ This reference records the setting and transfer contracts used by the TUI. It does not add a settings format or change transfer authorization.
4
+
5
+ ## Settings
6
+
7
+ All settings are private to this device. There are no project overrides. Defaults and validation are defined in `src/settings/settings-validation.ts` and `src/settings/config.ts`; mutation is owned by `settings-management.ts` and `settings-store.ts`.
8
+
9
+ | Row | Persisted fields and default | Scope | Save and effective time |
10
+ | --- | --- | --- | --- |
11
+ | Included paths | `sync.include`, explicit array | Current setup | Draft, then exact Save/Discard review; subsequent operations |
12
+ | Compare included paths | `sync.include` | Current setup | Review remote policy and fresh head before save; does not transfer files without explicit Continue |
13
+ | Machine-local fields | `sync.localFields`, absent | Current setup | Input and compatibility review; subsequent snapshots exclude whole settings.json root fields; directional migration required |
14
+ | Automatic sync | `sync.automatic`, explicit boolean; setup proposes Off | Current setup | Immediate save; next session start checks once; subsequent shutdown pushes all selected content only if sessions included; reload skips shutdown push |
15
+ | Startup transfer | `sync.automaticTransfer`, false | Current setup | Immediate save; enabling schedules next session start; independent of Automatic sync |
16
+ | On setup switch | `onSwitch`, `ask-before-pull` | All setups | Immediate save; subsequent switches |
17
+ | Storage location | `storage.bucket`, `branch`, `path` as applicable | Current setup | Reviewed save; changes future destination without moving remote history |
18
+ | Manage sync setups | `syncSetups`, `activeSyncSetup` | This device | Existing reviewed catalog flows; replacing/removing the current setup closes its old settings owner |
19
+ | Manage storage connections | `storageConnections` | Shared by named setups | Existing affected-setup review; changes future operations |
20
+ | Merge settings fields | `sync.mergeSettings`, false | Current setup | Immediate save; experimental merge on subsequent sync with a verified ancestor |
21
+ | Content merge / partial progress | `sync.mergeContent`, `sync.partialSync`, false/false | Current setup | Explicit compatibility review and atomic save; Off, Merge only, Merge + partial, Partial only preserve all combinations |
22
+ | Skip secret scan | `skipSecretScan`, false | All setups | Immediate save; subsequent manual/automatic pushes; Diagnostics still scans |
23
+ | Show sync status | `showStatus`, true | All setups | Immediate save and immediate status visibility change; notifications/widgets remain |
24
+ | Debug startup timings | `debug`, false | All setups | Immediate save; fixed-label duration notifications from the next session start; TUI/RPC only |
25
+
26
+ Opening foreground Settings cancels and drains startup work. Back does not undo successful saves; cancelling a draft or compatibility review leaves that change unsaved. A failed save retains the previous document and effective/displayed value. Completed transfers are never undone by changing a preference.
27
+
28
+ Writers queue the complete read-modify-write operation, hold the cross-process lock through validation and same-directory atomic replacement, preserve unknown fields and private permissions, and refuse invalid existing files. UI changes do not replace those writers.
29
+
30
+ ## Presentation decisions
31
+
32
+ Settings opens four task categories: Sync content, Sync behavior, Storage & setups, and Advanced. All settings (search) exposes the same rows for cross-category native-style typing-to-search. Each category uses the same row definitions and action handlers; navigation headings are not fake settings. Back returns one level; hard Ctrl+C closes the flow. No `/ Search` hint is added because slash is ordinary search text.
33
+
34
+ The existing public Kit settings adapter is retained rather than adding a second selector or accessing private Pi fields. Installed `SettingsList` provides `updateValue()` and `selectItem()`, but has no injected keybinding or awaited-save/rejection contract and does not expose search focus. Kit already owns ordered pending changes, rollback, retained search/selection, IME focus, bounded frames, and clipped mouse coordinates. This is an intentional existing deviation from the literal native-component convention. No new Kit API, dependency floor, or release is required. A package-owned public rendering boundary returns no lines at zero width and retains the selected row (or search when nothing matches) below five content rows; compact mouse events map back to the original rendered row. It does not access private component state or add key handlers. Search placeholder and position-footer relocation remain unsupported by that public API; the existing native-style hints and position indicator are retained rather than patched through private fields.
35
+
36
+ Last applied shows a compact opaque snapshot ID and explicitly states that application time is unavailable. History retains the full recorded ID even when the remote history no longer lists that snapshot. Snapshot `createdAt` is not a local application time; opening the manager performs no remote timestamp fetch.
37
+
38
+ Startup transfer summaries report the preference and prerequisites, not a claim that a transfer is eligible now. Runtime evidence in `merged-sync.ts` and `s3/s3-backend.ts` permits Git, conditional WebDAV, and R2/S3 with verified conditional writes. `read-check-write-verify` publication is refused for automatic operations. Baseline, matching authoritative include policy, idle ownership, and conflict/secret checks remain mandatory. This corrects the earlier settings reference's manual-only R2/S3 statement without expanding backend authorization.
39
+
40
+ ## Transfer review matrix
41
+
42
+ | Branch | Review and effect | Freshness / acceptance evidence |
43
+ | --- | --- | --- |
44
+ | Sync now, empty selection | Warning; no useful transfer | `empty-policy-acceptance.test.ts` |
45
+ | Sync now, first sync / empty remote | Existing decision or reviewed directional Pull/Push | `sync.test.ts`, `sync-decision.test.ts`; selection/policy checks remain |
46
+ | Sync now, equal files | Existing no-op or baseline acceptance; not represented as file changes | `sync.test.ts`, `sync-inspection.test.ts` |
47
+ | Pull | Remote → local counts and paths; protected loaded session excluded; backup before apply | `sync-mutations.ts`: settings, local hashes, head and baseline rechecked before commit; `recovery-pull-freshness.test.ts`, `local-field-pull-policy.test.ts` |
48
+ | Push | Local → remote active snapshot counts and paths; preserve eligible unmanaged content; removals do not erase retained history | `sync-mutations.ts`: fresh interactive before-image, local hashes/settings/baseline rechecked, expected-head publication; `backend-orchestration.test.ts`, `review-feedback.test.ts` |
49
+ | Forced Pull/Push | Same directional review, not a bypass of conditional publication | Refreshed force-push plan requires new approval; migration confirmation remains mandatory even with `--yes`; `sync-state-revision.test.ts`, `local-field-migration.test.ts` |
50
+ | Merge / partial progress | Effective before/after snapshots, local/remote counts, conflicts/withheld groups, backups and publication | Existing scrollable review; recheck settings, hashes, state and remote before journal/apply; `merged-sync-confirmation.test.ts`, `partial-directional-progress.test.ts`, `merged-sync-summary.test.ts` |
51
+ | Conflicts | Review complete dependency group and separately choose authority | Artifact fingerprint, group versions, baseline, config, owner and head checks; `conflict-confirmation.test.ts`, `conflict-preview.test.ts` |
52
+ | Case-variant cleanup | Pull/rollback include the apply engine’s additional top-level alias removals, even when canonical snapshot bytes are unchanged | `apply-review.ts` uses the authoritative apply planner, streams file hashes, records symlink identity without following its target, and rechecks the complete alias set before commit; `transfer-preview.test.ts` covers changed, added, removed, cancelled and symlink aliases plus descriptor cleanup on cancellation. Merge preflight rejects noncanonical changed targets instead of deleting aliases. |
53
+ | History rollback | Historical snapshot → local and active remote head; backup and protected-session summary | Settings, local bytes, current head, selected backend/setup and baseline rechecked; `history-confirmation-cancellation.test.ts`, `history-cancellation.test.ts`, `rollback-field-policy.test.ts` |
54
+ | Explicit `--yes` | Existing documented review bypass only | Mandatory migration and operation safety remain; existing route suites |
55
+ | Automatic startup / shutdown | No new modal; existing opt-in, capability, idle, all-or-nothing conflict and secret policies | `automatic-transfer.test.ts`, `startup-check.test.ts`, `backend-orchestration.test.ts` |
56
+
57
+ TUI Pull and Push reviews reuse the existing scrollable safe-default confirmation and exact Details document; RPC and History rollback retain their observable confirmation protocols. The existing History dialog ownership/cancellation flow is not replaced. All changed counts come from effective snapshot differences rather than raw merge decisions. Protected paths, preserved remote files, merged bytes, and withheld paths must be accounted for before rendering. Cancellation performs no transfer; pre-commit preparation may retain recovery evidence. After the commit boundary, existing completion/recovery rules apply instead of promising undo.
58
+
59
+ ## Review boundaries
60
+
61
+ This change does not touch model prompts, tool exposure, shared-terminal handoff, settings formats, credentials, or default sync policies. Existing startup/session signals and Kit/custom-interaction disposal own new UI calls. Any awaited review is followed by owner validation, and each mutating operation revalidates its mutable inputs before its existing commit boundary. Status and differences actions use existing routes; manager opening/refresh remains observational and does not contact the remote. Diagnostics discloses its WebDAV write/repair behavior before execution.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.54.2",
3
+ "version": "0.56.0",
4
4
  "description": "Pi extension that syncs Pi configuration through Git, WebDAV, Cloudflare R2, or S3-compatible storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,7 +52,8 @@
52
52
  "typescript": "7.0.2"
53
53
  },
54
54
  "peerDependencies": {
55
- "@earendil-works/pi-coding-agent": ">=1.0.2",
55
+ "@earendil-works/pi-ai": "*",
56
+ "@earendil-works/pi-coding-agent": ">=1.1.0",
56
57
  "@earendil-works/pi-tui": "*"
57
58
  },
58
59
  "repository": {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { posixJoin } from "../../paths.js";
5
5
  import type { Snapshot, SnapshotFile } from "../../snapshot/snapshot-types.js";
6
6
  import { stateDir } from "../../state/state-directory.js";
7
+ import { throwIfAborted } from "../../sync/signals.js";
7
8
  import { syncErrorGuidance } from "../../sync/sync-error-guidance.js";
8
9
  import type { ResolvedGitBackend } from "../backend-types.js";
9
10
  import {
@@ -781,7 +782,3 @@ function sha256(value: Buffer) {
781
782
  function abortReason(signal: AbortSignal) {
782
783
  return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
783
784
  }
784
-
785
- function throwIfAborted(signal?: AbortSignal) {
786
- if (signal?.aborted) throw abortReason(signal);
787
- }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import process from "node:process";
3
+ import { throwIfAborted } from "../../sync/signals.js";
3
4
 
4
5
  const DEFAULT_TIMEOUT_MS = 30_000;
5
6
  const DEFAULT_OUTPUT_LIMIT = 1024 * 1024;
@@ -252,8 +253,3 @@ export async function readGitBlobs(
252
253
  });
253
254
  return parseGitBlobBatch(result.stdout, objects.length, options.maxOutputBytes);
254
255
  }
255
-
256
- function throwIfAborted(signal?: AbortSignal) {
257
- if (!signal?.aborted) return;
258
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
259
- }
@@ -4,9 +4,11 @@ import { sessionTokenWarnings } from "../../settings/config.js";
4
4
  import { decodeSnapshot, encodeSnapshot } from "../../snapshot/snapshot-codec.js";
5
5
  import type { Snapshot } from "../../snapshot/snapshot-types.js";
6
6
  import { validatePortableSnapshot } from "../../sync/local-fields.js";
7
+ import { throwIfAborted } from "../../sync/signals.js";
7
8
  import { syncErrorGuidance } from "../../sync/sync-error-guidance.js";
8
9
  import { portableSnapshotSelection } from "../../sync/sync-policy.js";
9
10
  import type { LatestPointer, RemoteObject, ResolvedS3Backend } from "../backend-types.js";
11
+ import { createLatestPointer, remoteHistoryEntry } from "../snapshot-pointer.js";
10
12
  import {
11
13
  type BackendDiagnostic,
12
14
  type ExpectedRemoteHead,
@@ -318,16 +320,7 @@ export function storageRoot(config: ResolvedS3Backend) {
318
320
  }
319
321
 
320
322
  export function pointerFor(config: ResolvedS3Backend, snapshot: Snapshot, checksum: string): LatestPointer {
321
- return {
322
- version: VERSION,
323
- profile: config.destination.namespace,
324
- snapshot: snapshot.id,
325
- sha256: checksum,
326
- createdAt: snapshot.createdAt,
327
- machine: snapshot.machine,
328
- syncSessions: snapshot.syncSessions === true || snapshot.files.some((file) => file.path.startsWith("sessions/")),
329
- ...(snapshot.selection === undefined ? {} : { selection: snapshot.selection }),
330
- };
323
+ return createLatestPointer(config.destination.namespace, snapshot, checksum);
331
324
  }
332
325
 
333
326
  export function s3BackendIdentity(config: ResolvedS3Backend) {
@@ -365,16 +358,6 @@ function trimSlashes(value: string) {
365
358
  return value.replace(/^\/+|\/+$/g, "");
366
359
  }
367
360
 
368
- function remoteHistoryEntry(pointer: LatestPointer): RemoteHistoryEntry {
369
- return {
370
- snapshotRef: pointer.snapshot,
371
- snapshotId: pointer.snapshot,
372
- createdAt: pointer.createdAt,
373
- machine: pointer.machine,
374
- syncSessions: pointer.syncSessions === true,
375
- };
376
- }
377
-
378
361
  function remoteHead(pointer: LatestPointer, identity: string, etag?: string): RemoteHead {
379
362
  return {
380
363
  snapshotRef: pointer.snapshot,
@@ -529,11 +512,6 @@ function hasControlCharacter(value: string) {
529
512
  });
530
513
  }
531
514
 
532
- function throwIfAborted(signal?: AbortSignal) {
533
- if (!signal?.aborted) return;
534
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
535
- }
536
-
537
515
  function sha256(value: Buffer) {
538
516
  return createHash("sha256").update(value).digest("hex");
539
517
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash, createHmac } from "node:crypto";
2
2
  import { setTimeout as sleep } from "node:timers/promises";
3
3
  import { encodeKey, posixJoin } from "../../paths.js";
4
+ import { retryAfter } from "../../sync/continuous-errors.js";
4
5
  import type { RemoteObject, ResolvedS3Backend } from "../backend-types.js";
5
6
 
6
7
  const MAX_JSON_RESPONSE_BYTES = 1024 * 1024;
@@ -34,6 +35,7 @@ export class S3HttpError extends Error {
34
35
  constructor(
35
36
  message: string,
36
37
  readonly status: number,
38
+ readonly retryAfterMs?: number,
37
39
  ) {
38
40
  super(message);
39
41
  this.name = "S3HttpError";
@@ -81,7 +83,11 @@ export class S3Client {
81
83
  const object = await this.request("GET", key);
82
84
  if (object.status === 404) return { missing: true };
83
85
  if (!object.ok) {
84
- throw new S3HttpError(`S3 GET failed (${object.status}): ${await this.readErrorText(object)}`, object.status);
86
+ throw new S3HttpError(
87
+ `S3 GET failed (${object.status}): ${await this.readErrorText(object)}`,
88
+ object.status,
89
+ retryAfter(object),
90
+ );
85
91
  }
86
92
  const body = await readBoundedText(object, MAX_JSON_RESPONSE_BYTES, "S3 JSON response");
87
93
  // R2 can intermittently return an empty 200 body (read-after-write
@@ -111,7 +117,11 @@ export class S3Client {
111
117
  const object = await this.request("GET", key);
112
118
  if (object.status === 404) return { missing: true };
113
119
  if (!object.ok) {
114
- throw new S3HttpError(`S3 GET failed (${object.status}): ${await this.readErrorText(object)}`, object.status);
120
+ throw new S3HttpError(
121
+ `S3 GET failed (${object.status}): ${await this.readErrorText(object)}`,
122
+ object.status,
123
+ retryAfter(object),
124
+ );
115
125
  }
116
126
  const buffer = await readBoundedBuffer(object, MAX_SNAPSHOT_RESPONSE_BYTES, "S3 snapshot response");
117
127
  // R2 can intermittently return an empty 200 body on a long-lived
@@ -158,6 +168,7 @@ export class S3Client {
158
168
  throw new S3HttpError(
159
169
  `S3 PUT failed (${response.status}): ${await this.readErrorText(response)}`,
160
170
  response.status,
171
+ retryAfter(response),
161
172
  );
162
173
  }
163
174
  await response.body?.cancel();
@@ -169,6 +180,7 @@ export class S3Client {
169
180
  throw new S3HttpError(
170
181
  `S3 DELETE failed (${response.status}): ${await this.readErrorText(response)}`,
171
182
  response.status,
183
+ retryAfter(response),
172
184
  );
173
185
  }
174
186
  await response.body?.cancel();
@@ -0,0 +1,27 @@
1
+ import type { Snapshot } from "../snapshot/snapshot-types.js";
2
+ import type { LatestPointer } from "./backend-types.js";
3
+ import type { RemoteHistoryEntry } from "./sync-backend.js";
4
+
5
+ /** Shared latest/history metadata for the S3 and WebDAV object-store format. */
6
+ export function createLatestPointer(namespace: string, snapshot: Snapshot, checksum: string): LatestPointer {
7
+ return {
8
+ version: 1,
9
+ profile: namespace,
10
+ snapshot: snapshot.id,
11
+ sha256: checksum,
12
+ createdAt: snapshot.createdAt,
13
+ machine: snapshot.machine,
14
+ syncSessions: snapshot.syncSessions === true || snapshot.files.some((file) => file.path.startsWith("sessions/")),
15
+ ...(snapshot.selection === undefined ? {} : { selection: snapshot.selection }),
16
+ };
17
+ }
18
+
19
+ export function remoteHistoryEntry(pointer: LatestPointer): RemoteHistoryEntry {
20
+ return {
21
+ snapshotRef: pointer.snapshot,
22
+ snapshotId: pointer.snapshot,
23
+ createdAt: pointer.createdAt,
24
+ machine: pointer.machine,
25
+ syncSessions: pointer.syncSessions === true,
26
+ };
27
+ }
@@ -2,9 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { decodeSnapshot, encodeSnapshot } from "../../snapshot/snapshot-codec.js";
3
3
  import type { Snapshot } from "../../snapshot/snapshot-types.js";
4
4
  import { validatePortableSnapshot } from "../../sync/local-fields.js";
5
+ import { throwIfAborted } from "../../sync/signals.js";
5
6
  import { syncErrorGuidance } from "../../sync/sync-error-guidance.js";
6
7
  import { portableSnapshotSelection } from "../../sync/sync-policy.js";
7
8
  import type { LatestPointer, RemoteObject, ResolvedWebDavBackend } from "../backend-types.js";
9
+ import { createLatestPointer, remoteHistoryEntry } from "../snapshot-pointer.js";
8
10
  import {
9
11
  type BackendDiagnostic,
10
12
  type ExpectedRemoteHead,
@@ -84,7 +86,7 @@ export class WebDavSyncBackend implements SyncBackend {
84
86
  await client.ensureCollection(snapshotsPath(this.config));
85
87
  const encoded = await encodeSnapshot(snapshot);
86
88
  throwIfAborted(options.signal);
87
- const pointer = pointerFor(this.config, snapshot, sha256(encoded));
89
+ const pointer = createLatestPointer(this.config.destination.namespace, snapshot, sha256(encoded));
88
90
  const stagedPath = snapshotPath(this.config, snapshot.id);
89
91
  try {
90
92
  await client.putBuffer(stagedPath, encoded, "application/gzip", { ifAbsent: true });
@@ -392,19 +394,6 @@ function webDavStorageLocation(config: ResolvedWebDavBackend) {
392
394
  return `${url.host} · ${config.destination.path}`;
393
395
  }
394
396
 
395
- function pointerFor(config: ResolvedWebDavBackend, snapshot: Snapshot, checksum: string): LatestPointer {
396
- return {
397
- version: VERSION,
398
- profile: config.destination.namespace,
399
- snapshot: snapshot.id,
400
- sha256: checksum,
401
- createdAt: snapshot.createdAt,
402
- machine: snapshot.machine,
403
- syncSessions: snapshot.syncSessions === true || snapshot.files.some((file) => file.path.startsWith("sessions/")),
404
- ...(snapshot.selection === undefined ? {} : { selection: snapshot.selection }),
405
- };
406
- }
407
-
408
397
  function remoteHead(pointer: LatestPointer, identity: string, etag?: string): RemoteHead {
409
398
  return {
410
399
  snapshotRef: pointer.snapshot,
@@ -490,16 +479,6 @@ function requireHistory(value: { version: number; snapshots: LatestPointer[] } |
490
479
  return pointers;
491
480
  }
492
481
 
493
- function remoteHistoryEntry(pointer: LatestPointer): RemoteHistoryEntry {
494
- return {
495
- snapshotRef: pointer.snapshot,
496
- snapshotId: pointer.snapshot,
497
- createdAt: pointer.createdAt,
498
- machine: pointer.machine,
499
- syncSessions: pointer.syncSessions === true,
500
- };
501
- }
502
-
503
482
  function assertSnapshotIdentity(snapshot: Snapshot, namespace: string) {
504
483
  validatePortableSnapshot(snapshot);
505
484
  if (
@@ -637,11 +616,6 @@ function sha256(value: Buffer) {
637
616
  return createHash("sha256").update(value).digest("hex");
638
617
  }
639
618
 
640
- function throwIfAborted(signal?: AbortSignal) {
641
- if (!signal?.aborted) return;
642
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
643
- }
644
-
645
619
  function errorMessage(error: unknown) {
646
620
  if (error instanceof WebDavHttpError) return error.message;
647
621
  return error instanceof Error ? error.message : String(error);
@@ -1,4 +1,6 @@
1
1
  import { XMLParser } from "fast-xml-parser";
2
+ import { ContinuousError, continuousFailure, retryAfter } from "../../sync/continuous-errors.js";
3
+ import { throwIfAborted } from "../../sync/signals.js";
2
4
  import type { RemoteObject, ResolvedWebDavBackend } from "../backend-types.js";
3
5
 
4
6
  const JSON_LIMIT = 1024 * 1024;
@@ -24,6 +26,7 @@ export class WebDavHttpError extends Error {
24
26
  message: string,
25
27
  readonly status: number,
26
28
  options?: ErrorOptions,
29
+ readonly retryAfterMs?: number,
27
30
  ) {
28
31
  super(message, options);
29
32
  this.name = "WebDavHttpError";
@@ -231,7 +234,12 @@ export class WebDavClient {
231
234
  .replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ")
232
235
  .trim();
233
236
  const detail = body ? `: ${redactText(body, this.config)}` : "";
234
- throw new WebDavHttpError(`WebDAV ${action} failed with HTTP ${response.status}${detail}`, response.status);
237
+ throw new WebDavHttpError(
238
+ `WebDAV ${action} failed with HTTP ${response.status}${detail}`,
239
+ response.status,
240
+ undefined,
241
+ retryAfter(response),
242
+ );
235
243
  }
236
244
  }
237
245
 
@@ -342,7 +350,8 @@ function safeUrl(url: URL) {
342
350
 
343
351
  function redactError(error: unknown, config: ResolvedWebDavBackend) {
344
352
  const message = error instanceof Error ? error.message : String(error);
345
- return new Error(redactText(message, config));
353
+ const failure = continuousFailure(error);
354
+ return new ContinuousError(failure.kind, redactText(message, config), failure.delay);
346
355
  }
347
356
 
348
357
  function redactText(value: string, config: ResolvedWebDavBackend) {
@@ -359,8 +368,3 @@ function redactText(value: string, config: ResolvedWebDavBackend) {
359
368
  function escapeRegExp(value: string) {
360
369
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
361
370
  }
362
-
363
- function throwIfAborted(signal?: AbortSignal) {
364
- if (!signal?.aborted) return;
365
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
366
- }