@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
@@ -5,6 +5,7 @@ import { localConfigTemplate, readLocalConfigObject } from "../settings/settings
5
5
  import type { AnySyncConfig } from "../settings/settings-types.js";
6
6
  import { unlock, withLock } from "../state/lock.js";
7
7
  import { migrateLegacyStateDirectory, stateDirectoryMigrationNotice } from "../state/state-directory.js";
8
+ import { withAgentSyncLock } from "../sync/agent-sync.js";
8
9
  import { throwIfAborted } from "../sync/signals.js";
9
10
  import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
10
11
  import { isSyncDecisionRequiredError, SetupPullRequiresUiError } from "../sync/sync-errors.js";
@@ -13,6 +14,7 @@ import { RemoteSelectionMismatchError } from "../sync/sync-policy.js";
13
14
  import { automaticSyncSummary } from "../ui/automatic-sync-summary.js";
14
15
  import type { RunRouteResult } from "../ui/cancellable-operation.js";
15
16
  import { trackOperationDialogs } from "../ui/operation-dialogs.js";
17
+ import { startupTransferSummary } from "../ui/sync-behavior.js";
16
18
  import { setSyncStatus } from "../ui/sync-status.js";
17
19
  import { safeTerminalText } from "../ui/terminal-text.js";
18
20
  import { parseOptions, resolveSyncCommand, splitArgs, usage, validateCommandOptions } from "./command.js";
@@ -48,7 +50,7 @@ export async function executeRecoveryCommand(
48
50
  const outcome = await withLock("pull", () => operations.pull(ctx, options));
49
51
  return { kind: "completed", ...(outcome ? { outcome } : {}) };
50
52
  }
51
- await withLock("sync", () => operations.syncBoth(ctx, options));
53
+ await withAgentSyncLock(ctx, options, (current) => operations.syncBoth(ctx, current));
52
54
  return { kind: "completed" };
53
55
  } catch (error) {
54
56
  if (signal?.aborted) return { kind: "failed" };
@@ -155,7 +157,7 @@ export async function executeCommand(
155
157
  case "sync": {
156
158
  const operations = await loaders.operations();
157
159
  throwIfAborted(options.signal);
158
- await withLock("sync", () => operations.syncBoth(ctx, options));
160
+ await withAgentSyncLock(ctx, options, (current) => operations.syncBoth(ctx, current));
159
161
  return { kind: "completed" };
160
162
  }
161
163
  case "history": {
@@ -256,7 +258,9 @@ async function showConfig(ctx: ExtensionCommandContext, options: CommandOptions)
256
258
  `automatic sync: ${automaticSyncSummary(config.automatic)}`,
257
259
  `settings field merge: ${config.mergeSettings ? "On (experimental)" : "Off"}`,
258
260
  `machine-local settings.json fields: ${(config.localFields ?? []).map(safeTerminalText).join(", ") || "none"}`,
259
- `automatic transfer at startup: ${config.automaticTransfer ? "On — selected-file upload/replace/delete; no reload" : "Off"}`,
261
+ `startup transfer: ${startupTransferSummary(config)}`,
262
+ `agent mode: ${config.agentMode ? "On (experimental)" : "Off"}`,
263
+ `continuous sync: ${config.continuous ? (config.agentMode ? "On (experimental)" : "paused; Agent mode is Off") : "Off"}`,
260
264
  `included content: ${config.include.join(", ") || "none"}`,
261
265
  `sessions: ${config.include.includes("sessions") ? "included" : "not included"}`,
262
266
  `settings file: ${localConfigPath()}`,
@@ -13,7 +13,7 @@ import {
13
13
  syncAttentionMatchesConfig,
14
14
  } from "../ui/sync-attention.js";
15
15
  import { setSyncStatus } from "../ui/sync-status.js";
16
- import { parseOptions, splitArgs } from "./command.js";
16
+ import { parseOptions, splitArgs, validateCommandOptions } from "./command.js";
17
17
  import { executeCommand, executeRecoveryCommand } from "./command-execution.js";
18
18
 
19
19
  export async function handleCommand(
@@ -22,6 +22,7 @@ export async function handleCommand(
22
22
  sessionSignal: AbortSignal,
23
23
  loaders: SyncLoaders,
24
24
  attention: SyncAttentionController,
25
+ onExplicitSync?: (setup?: string) => void,
25
26
  ) {
26
27
  await reconcileObservation(attention, sessionSignal);
27
28
  if (sessionSignal.aborted) return;
@@ -31,8 +32,18 @@ export async function handleCommand(
31
32
  onCommit?: () => void,
32
33
  target?: string,
33
34
  onDialog?: (active: boolean) => void,
34
- ) =>
35
- executeCommand(
35
+ ) => {
36
+ const [command, ...args] = splitArgs(route);
37
+ if (command === "sync") {
38
+ try {
39
+ const options = parseOptions(args);
40
+ validateCommandOptions("sync", options);
41
+ onExplicitSync?.(target ?? options.setup);
42
+ } catch {
43
+ /* Invalid routes do not request an automatic retry. */
44
+ }
45
+ }
46
+ return executeCommand(
36
47
  route,
37
48
  ctx,
38
49
  combineSignals(sessionSignal, signal),
@@ -41,6 +52,7 @@ export async function handleCommand(
41
52
  target,
42
53
  onDialog,
43
54
  );
55
+ };
44
56
  if (!rawArgs.trim()) {
45
57
  try {
46
58
  const { showSyncManager } = await import("../ui/manager-ui.js");
@@ -1,3 +1,7 @@
1
+ import type { AnySyncConfig } from "../settings/settings-types.js";
2
+ import type { AgentAcceptance } from "../sync/agent-request.js";
3
+ import type { StartupTimings } from "../sync/startup-timings.js";
4
+
1
5
  export interface CommandOptions {
2
6
  yes: boolean;
3
7
  force: boolean;
@@ -8,6 +12,12 @@ export interface CommandOptions {
8
12
  setup?: string;
9
13
  signal?: AbortSignal;
10
14
  onCommit?: () => void;
15
+ /** In-memory startup diagnostics only; never persisted or sent to a backend. */
16
+ startupTimings?: StartupTimings;
17
+ /** Internal validated resolver result; never parsed from command arguments. */
18
+ agentAcceptance?: AgentAcceptance;
19
+ /** Internal cycle authorization, ownership and durable request reservation; never command arguments. */
20
+ background?: { validate(config?: AnySyncConfig): void; reserve(key: string): Promise<void> };
11
21
  args: string[];
12
22
  }
13
23
 
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { AUTOMATIC_SYNC_DESCRIPTION, STARTUP_TRANSFER_DESCRIPTION } from "../ui/sync-behavior.js";
2
3
  import type { CommandArgumentCompletion, CommandOptions } from "./command-types.js";
3
4
 
4
5
  const YES_FLAG_COMPLETIONS: readonly CommandArgumentCompletion[] = [
@@ -232,8 +233,10 @@ export function usage() {
232
233
  return [
233
234
  "Usage: /sync <command>",
234
235
  `Commands: ${commands}`,
235
- "Automatic sync: background startup checks only; use /sync to review and transfer. At shutdown, selected content is pushed automatically only when sessions are included. Startup checks run in TUI/RPC, not print/JSON.",
236
+ `Automatic sync: ${AUTOMATIC_SYNC_DESCRIPTION}`,
237
+ `Startup transfer: ${STARTUP_TRANSFER_DESCRIPTION}`,
236
238
  "Automatic transfer at startup is a separate opt-in: one conflict-free established-baseline transfer at idle in TUI/RPC, with conditional/lease publication and no resource reload.",
239
+ "Experimental Continuous sync is a separate default-Off opt-in in Settings. It requires Agent mode, monitors while Pi is open, and can incur recurring network/model costs. Safety blocks and durable automatic request limits remain.",
237
240
  "Settings: use /sync init or edit storage connections and sync setups in ~/.pi/agent/pi-sync.json (or the configured Pi agent directory). Version 1 and version 2 settings are unsupported and are never rewritten.",
238
241
  ].join("\n");
239
242
  }
@@ -52,15 +52,18 @@ function configFromSettings(settings: PiSyncSettingsV3, setupName?: string): Any
52
52
  `Invalid pi-sync settings: sync setup “${selectedName}” references missing storage connection “${connectionName}”.`,
53
53
  );
54
54
  }
55
- return resolveSyncConfig(
56
- selectedName,
57
- setup,
58
- connectionName,
59
- connection,
60
- settings.onSwitch,
61
- settings.skipSecretScan ?? false,
62
- settings.showStatus ?? true,
63
- );
55
+ return {
56
+ ...resolveSyncConfig(
57
+ selectedName,
58
+ setup,
59
+ connectionName,
60
+ connection,
61
+ settings.onSwitch,
62
+ settings.skipSecretScan ?? false,
63
+ settings.showStatus ?? true,
64
+ ),
65
+ debug: settings.debug ?? false,
66
+ };
64
67
  }
65
68
 
66
69
  export type SyncSetupStorageReview = Pick<
@@ -77,6 +80,8 @@ export async function loadPartialConfig(setupName?: string): Promise<PartialConf
77
80
  include: [...config.include],
78
81
  automatic: config.automatic,
79
82
  automaticTransfer: config.automaticTransfer ?? false,
83
+ agentMode: config.agentMode ?? false,
84
+ continuous: config.continuous ?? false,
80
85
  onSwitch: config.onSwitch,
81
86
  showStatus: config.showStatus,
82
87
  };
@@ -112,10 +117,13 @@ export function syncConfigReviewIdentity(config: AnySyncConfig) {
112
117
  config.include,
113
118
  config.automatic,
114
119
  config.automaticTransfer ?? false,
120
+ config.agentMode ?? false,
115
121
  config.localFields,
116
122
  config.mergeSettings ?? false,
117
123
  config.mergeContent ?? false,
118
124
  config.partialSync ?? false,
125
+ // Preserve existing Off identities and cached candidates across the upgrade.
126
+ ...(config.continuous ? ["continuous"] : []),
119
127
  ]);
120
128
  }
121
129
 
@@ -126,7 +134,7 @@ export function syncConfigReviewFingerprint(config: AnySyncConfig) {
126
134
  /** Internal freshness token, including credentials; never display or persist it. */
127
135
  export function syncCheckConfigFingerprint(config: AnySyncConfig) {
128
136
  return createHash("sha256")
129
- .update(JSON.stringify({ ...config, showStatus: undefined }))
137
+ .update(JSON.stringify({ ...config, showStatus: undefined, debug: undefined }))
130
138
  .digest("hex");
131
139
  }
132
140
 
@@ -163,6 +171,8 @@ function resolveSyncConfig(
163
171
  include,
164
172
  automatic: setup.sync.automatic,
165
173
  automaticTransfer: setup.sync.automaticTransfer ?? false,
174
+ agentMode: setup.sync.agentMode ?? false,
175
+ continuous: setup.sync.continuous ?? false,
166
176
  localFields: setup.sync.localFields === undefined ? undefined : normalizeLocalFields(setup.sync.localFields),
167
177
  mergeSettings: setup.sync.mergeSettings ?? false,
168
178
  mergeContent: setup.sync.mergeContent ?? false,
@@ -1,6 +1,3 @@
1
- import { randomUUID } from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
1
  import { localConfigPath, readMigratingLocalConfigDocument, updateLocalConfigDocument } from "./config-file.js";
5
2
  import type { OnSwitchAction, PiSyncSettingsV3 } from "./settings-types.js";
6
3
  import { DEFAULT_ON_SWITCH, validateSettingsDocument } from "./settings-validation.js";
@@ -30,6 +27,7 @@ export function localConfigTemplate(): PiSyncSettingsV3 {
30
27
  onSwitch: DEFAULT_ON_SWITCH,
31
28
  skipSecretScan: false,
32
29
  showStatus: true,
30
+ debug: false,
33
31
  storageConnections: {},
34
32
  syncSetups: {},
35
33
  };
@@ -62,34 +60,3 @@ export function updateLocalConfig(update: (current: PiSyncSettingsV3) => PiSyncS
62
60
  async function performLocalConfigUpdate(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
63
61
  return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
64
62
  }
65
-
66
- export async function writeLocalConfigObject(value: PiSyncSettingsV3 | Record<string, unknown>) {
67
- validateSettingsDocument(value as Record<string, unknown>);
68
- const configPath = localConfigPath();
69
- await fs.mkdir(path.dirname(configPath), { recursive: true });
70
- try {
71
- const stat = await fs.lstat(configPath);
72
- if (stat.isSymbolicLink()) throw new Error(`Refusing to overwrite symlinked pi-sync settings: ${configPath}`);
73
- if (!stat.isFile()) throw new Error(`pi-sync settings are not a regular file: ${configPath}`);
74
- } catch (error) {
75
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
76
- }
77
- const temporaryPath = path.join(
78
- path.dirname(configPath),
79
- `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
80
- );
81
- let handle: fs.FileHandle | undefined;
82
- try {
83
- handle = await fs.open(temporaryPath, "wx", 0o600);
84
- await handle.writeFile(`${JSON.stringify(value, null, "\t")}\n`, "utf8");
85
- if (process.platform !== "win32") await handle.chmod(0o600);
86
- await handle.sync();
87
- await handle.close();
88
- handle = undefined;
89
- await fs.rename(temporaryPath, configPath);
90
- } catch (error) {
91
- await handle?.close().catch(() => undefined);
92
- await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
93
- throw error;
94
- }
95
- }
@@ -81,6 +81,10 @@ export interface SyncPolicySettings {
81
81
  automatic: boolean;
82
82
  /** Explicit opt-in, separate from the legacy observe/shutdown policy. */
83
83
  automaticTransfer?: boolean;
84
+ /** Experimental autonomous merged-sync authorization. */
85
+ agentMode?: boolean;
86
+ /** Separate explicit authorization for recurring background work. */
87
+ continuous?: boolean;
84
88
  /** Version 4: settings.json root fields never uploaded. */
85
89
  localFields?: string[];
86
90
  mergeSettings?: boolean;
@@ -101,6 +105,7 @@ export interface PiSyncSettingsV3 {
101
105
  onSwitch: OnSwitchAction;
102
106
  skipSecretScan?: boolean;
103
107
  showStatus?: boolean;
108
+ debug?: boolean;
104
109
  storageConnections: Record<string, StorageConnectionSettings>;
105
110
  syncSetups: Record<string, SyncSetupSettings>;
106
111
  [key: string]: unknown;
@@ -115,6 +120,8 @@ export interface SyncConfig<Backend extends ResolvedSyncBackend = ResolvedS3Back
115
120
  include: string[];
116
121
  automatic: boolean;
117
122
  automaticTransfer?: boolean;
123
+ agentMode?: boolean;
124
+ continuous?: boolean;
118
125
  localFields?: string[];
119
126
  mergeSettings?: boolean;
120
127
  mergeContent?: boolean;
@@ -122,6 +129,7 @@ export interface SyncConfig<Backend extends ResolvedSyncBackend = ResolvedS3Back
122
129
  onSwitch: OnSwitchAction;
123
130
  skipSecretScan: boolean;
124
131
  showStatus: boolean;
132
+ debug?: boolean;
125
133
  backend: Backend;
126
134
  }
127
135
 
@@ -137,6 +145,8 @@ export interface PartialConfig {
137
145
  include: string[];
138
146
  automatic: boolean;
139
147
  automaticTransfer?: boolean;
148
+ agentMode?: boolean;
149
+ continuous?: boolean;
140
150
  onSwitch: OnSwitchAction;
141
151
  showStatus: boolean;
142
152
  bucket?: string;
@@ -68,6 +68,9 @@ export function validateSettingsDocument(value: Record<string, unknown>): PiSync
68
68
  if (value.showStatus !== undefined && typeof value.showStatus !== "boolean") {
69
69
  throw new Error("Invalid pi-sync settings: showStatus must be boolean.");
70
70
  }
71
+ if (value.debug !== undefined && typeof value.debug !== "boolean") {
72
+ throw new Error("Invalid pi-sync settings: debug must be boolean.");
73
+ }
71
74
  const storageConnections = requireNamedObjectMap(
72
75
  value.storageConnections,
73
76
  "storageConnections",
@@ -216,7 +219,7 @@ function validateSyncSetup(name: string, value: Record<string, unknown>, connect
216
219
  const localFields = normalizeLocalFields(sync.localFields);
217
220
  if (localFields.length > 0 && !include.includes("settings.json"))
218
221
  throw new Error("Nonempty localFields requires settings.json in sync.include.");
219
- for (const key of ["mergeContent", "partialSync"])
222
+ for (const key of ["mergeContent", "partialSync", "agentMode", "continuous"])
220
223
  if (sync[key] !== undefined && typeof sync[key] !== "boolean") throw new Error(`${key} must be boolean.`);
221
224
  if (sync.mergeSettings !== undefined && typeof sync.mergeSettings !== "boolean")
222
225
  throw new Error("mergeSettings must be boolean.");
@@ -2,6 +2,7 @@ import { Readable, Writable } from "node:stream";
2
2
  import { pipeline } from "node:stream/promises";
3
3
  import { promisify } from "node:util";
4
4
  import { createGunzip, gzip } from "node:zlib";
5
+ import { throwIfAborted } from "../sync/signals.js";
5
6
  import { snapshotSelectionInclude } from "../sync/sync-policy.js";
6
7
  import type { Snapshot } from "./snapshot-types.js";
7
8
 
@@ -47,8 +48,3 @@ export async function decodeSnapshot(
47
48
  snapshotSelectionInclude(parsed);
48
49
  return parsed;
49
50
  }
50
-
51
- function throwIfAborted(signal?: AbortSignal) {
52
- if (!signal?.aborted) return;
53
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
54
- }
package/src/state/lock.ts CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
3
3
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
4
  import lockfile from "proper-lockfile";
5
5
  import type { CommandOptions } from "../commands/command-types.js";
6
+ import { throwIfAborted } from "../sync/signals.js";
6
7
  import { LOCK_GUARD_STALE_MS, LOCK_GUARD_UPDATE_MS } from "./lock-policy.js";
7
8
  import { LOCKFILE_FS_ADAPTER } from "./lockfile-fs.js";
8
9
  import type { LockFile } from "./state-types.js";
@@ -57,8 +58,11 @@ export async function withLock<T>(
57
58
  inspection = { status: "missing" };
58
59
  }
59
60
  if (inspection.status === "valid") {
60
- throw new Error(
61
- `pi-sync is already running (${inspection.lock.command}, pid ${inspection.lock.pid}, started ${inspection.lock.startedAt}).`,
61
+ throw Object.assign(
62
+ new Error(
63
+ `pi-sync is already running (${inspection.lock.command}, pid ${inspection.lock.pid}, started ${inspection.lock.startedAt}).`,
64
+ ),
65
+ { code: "ELOCKED" },
62
66
  );
63
67
  }
64
68
  if (inspection.status === "unreadable") {
@@ -220,11 +224,6 @@ async function unlockGuarded(ctx: ExtensionCommandContext, options: CommandOptio
220
224
  }
221
225
  }
222
226
 
223
- function throwIfAborted(signal?: AbortSignal) {
224
- if (!signal?.aborted) return;
225
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
226
- }
227
-
228
227
  async function releaseGuard(guard: Guard) {
229
228
  try {
230
229
  await guard.release();
@@ -261,11 +260,15 @@ async function describeHeldLock() {
261
260
  return new Error("pi-sync lock owner exited; retry shortly while the lock guard expires.");
262
261
  }
263
262
  if (current) {
264
- return new Error(
265
- `Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt}).`,
263
+ return Object.assign(
264
+ new Error(`Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt}).`),
265
+ { code: "ELOCKED" },
266
266
  );
267
267
  }
268
- return new Error("Pi-sync is currently running (lock metadata is unreadable or still being written).");
268
+ return Object.assign(
269
+ new Error("Pi-sync is currently running (lock metadata is unreadable or still being written)."),
270
+ { code: "ELOCKED" },
271
+ );
269
272
  }
270
273
 
271
274
  function isLockHeldError(error: unknown) {
@@ -66,7 +66,7 @@ export async function stageMergeBaseline(
66
66
  return (
67
67
  !collisions.has(file.path) &&
68
68
  accepted.lastFileHashes[file.path] === file.sha256 &&
69
- (file.path === "settings.json" || config.mergeContent) &&
69
+ (file.path === "settings.json" || config.mergeContent || (config.agentMode && isMergeTextPath(file.path))) &&
70
70
  eligible(file)
71
71
  );
72
72
  });
@@ -145,6 +145,24 @@ export async function readMergeAncestors(config: AnySyncConfig, state: SyncState
145
145
  }
146
146
  return record.files;
147
147
  }
148
+ /** A no-op must not skip seeding or repairing ancestors required by later merges. */
149
+ export async function hasCurrentMergeBaseline(config: AnySyncConfig, snapshot: Snapshot, state: SyncState) {
150
+ try {
151
+ const ancestors = await readMergeAncestors(config, state);
152
+ if (!ancestors) return false;
153
+ const hashes = new Map(ancestors.map((file) => [file.path, file.sha256]));
154
+ return snapshot.files.every(
155
+ (file) =>
156
+ !(file.path === "settings.json" || config.mergeContent || (config.agentMode && isMergeTextPath(file.path))) ||
157
+ !eligible(file) ||
158
+ hashes.get(file.path) === file.sha256,
159
+ );
160
+ } catch {
161
+ // Preserve the existing full path's validation and repair behavior for unknown evidence.
162
+ return false;
163
+ }
164
+ }
165
+
148
166
  export async function readMergeAncestor(config: AnySyncConfig, state: SyncState, filePath: string) {
149
167
  const file = (await readMergeAncestors(config, state))?.find((file) => file.path === filePath);
150
168
  return file ? Buffer.from(file.contentBase64, "base64") : undefined;
@@ -79,7 +79,14 @@ export async function migrateLegacyStateDirectory(): Promise<StateDirectoryPrepa
79
79
  realpath: false,
80
80
  stale: LOCK_GUARD_STALE_MS,
81
81
  });
82
- if (legacyGuardHeld || (await pathExists(legacyLock))) {
82
+ // Idle continuous owners do not retain the exclusive migration guard.
83
+ // A live renewable lease still prevents moving their private state root.
84
+ const schedulerHeld = await lockfile.check(path.join(legacyStateDir(), "continuous", "scheduler"), {
85
+ fs: LOCKFILE_FS_ADAPTER,
86
+ realpath: false,
87
+ stale: LOCK_GUARD_STALE_MS,
88
+ });
89
+ if (legacyGuardHeld || schedulerHeld || (await pathExists(legacyLock))) {
83
90
  return {
84
91
  status: "deferred",
85
92
  message: