@narumitw/pi-sync 0.52.1 → 0.53.1

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 (40) hide show
  1. package/README.md +3 -4
  2. package/dist/chunks/{chunk-WKPX2SIC.ts → chunk-FJGNDYGC.ts} +2 -2
  3. package/dist/chunks/{chunk-NLS2NV3K.ts → chunk-QDPED3XJ.ts} +67 -8
  4. package/dist/chunks/{chunk-NLS2NV3K.ts.map → chunk-QDPED3XJ.ts.map} +3 -3
  5. package/dist/chunks/{chunk-QHSQCK7L.ts → chunk-SDZCTCYQ.ts} +4 -4
  6. package/dist/chunks/{chunk-2CBF6SMT.ts → chunk-TRCSEIAQ.ts} +2 -2
  7. package/dist/chunks/{chunk-7D6RDCVC.ts → chunk-WFKUKVCP.ts} +2 -2
  8. package/dist/chunks/{chunk-UETBKTDI.ts → chunk-YDVQYUHE.ts} +1 -1
  9. package/dist/chunks/{chunk-UETBKTDI.ts.map → chunk-YDVQYUHE.ts.map} +2 -2
  10. package/dist/chunks/{conflict-review-4WPGCXFB.ts → conflict-review-JANNPKKS.ts} +4 -4
  11. package/dist/chunks/{git-backend-HLGKUQJT.ts → git-backend-FD7AVSK6.ts} +2 -2
  12. package/dist/chunks/{manager-result-dispatcher-VIDT3QBU.ts → manager-result-dispatcher-AAEAYX57.ts} +4 -4
  13. package/dist/chunks/{manager-ui-HSMHS3OK.ts → manager-ui-MHAHAKG3.ts} +7 -7
  14. package/dist/chunks/{remote-selection-ui-SMF5CLFO.ts → remote-selection-ui-Q2HUIBTL.ts} +3 -3
  15. package/dist/chunks/{s3-backend-DJ2NT3SZ.ts → s3-backend-AQGV6LFT.ts} +116 -12
  16. package/dist/chunks/s3-backend-AQGV6LFT.ts.map +7 -0
  17. package/dist/chunks/{sync-inspection-VBJF2FWC.ts → sync-inspection-K45QVLHB.ts} +3 -3
  18. package/dist/chunks/{sync-operations-RHNECOOG.ts → sync-operations-Y27F5T7W.ts} +5 -5
  19. package/dist/chunks/{webdav-backend-ETQGSI6I.ts → webdav-backend-TBILVEQK.ts} +2 -2
  20. package/dist/index.ts +5 -5
  21. package/package.json +1 -1
  22. package/src/backends/s3/s3-backend.ts +45 -6
  23. package/src/backends/s3/s3-client.ts +36 -7
  24. package/src/backends/s3/s3-conditions.ts +67 -0
  25. package/src/backends/sync-backend.ts +2 -0
  26. package/src/sync/merge-transfer-error.ts +72 -0
  27. package/src/sync/merged-sync.ts +8 -5
  28. package/dist/chunks/s3-backend-DJ2NT3SZ.ts.map +0 -7
  29. /package/dist/chunks/{chunk-WKPX2SIC.ts.map → chunk-FJGNDYGC.ts.map} +0 -0
  30. /package/dist/chunks/{chunk-QHSQCK7L.ts.map → chunk-SDZCTCYQ.ts.map} +0 -0
  31. /package/dist/chunks/{chunk-2CBF6SMT.ts.map → chunk-TRCSEIAQ.ts.map} +0 -0
  32. /package/dist/chunks/{chunk-7D6RDCVC.ts.map → chunk-WFKUKVCP.ts.map} +0 -0
  33. /package/dist/chunks/{conflict-review-4WPGCXFB.ts.map → conflict-review-JANNPKKS.ts.map} +0 -0
  34. /package/dist/chunks/{git-backend-HLGKUQJT.ts.map → git-backend-FD7AVSK6.ts.map} +0 -0
  35. /package/dist/chunks/{manager-result-dispatcher-VIDT3QBU.ts.map → manager-result-dispatcher-AAEAYX57.ts.map} +0 -0
  36. /package/dist/chunks/{manager-ui-HSMHS3OK.ts.map → manager-ui-MHAHAKG3.ts.map} +0 -0
  37. /package/dist/chunks/{remote-selection-ui-SMF5CLFO.ts.map → remote-selection-ui-Q2HUIBTL.ts.map} +0 -0
  38. /package/dist/chunks/{sync-inspection-VBJF2FWC.ts.map → sync-inspection-K45QVLHB.ts.map} +0 -0
  39. /package/dist/chunks/{sync-operations-RHNECOOG.ts.map → sync-operations-Y27F5T7W.ts.map} +0 -0
  40. /package/dist/chunks/{webdav-backend-ETQGSI6I.ts.map → webdav-backend-TBILVEQK.ts.map} +0 -0
package/README.md CHANGED
@@ -166,7 +166,7 @@ Shutdown behavior is unchanged: when **Automatic sync** is On and sessions are i
166
166
 
167
167
  **Settings → Automatic transfer at startup** controls the separate per-setup `sync.automaticTransfer` boolean, which defaults to `false`. It authorizes one conflict-free startup transfer at a validated idle boundary in TUI/RPC, including session replacement and `/reload`. Busy startup waits for `agent_settled`; foreground commands, a new agent run, replacement, and shutdown cancel and drain owned work. Changing the setting affects authorization immediately; enabling it schedules the next attempt on the next session start. There is no watcher or polling loop.
168
168
 
169
- A baseline and matching authoritative content list are required. First-source choice, changed selection, missing established remote, secrets, and real conflicts remain review barriers. Automatic transfer requires conditional or lease-protected publication: Git and verified WebDAV are supported; R2/S3 remains manual because its publication is read-check-write-verify. The existing **Automatic sync** setting and shutdown policy are unchanged when this new opt-in is absent.
169
+ A baseline and matching authoritative content list are required. First-source choice, changed selection, missing established remote, secrets, and real conflicts remain review barriers. Automatic transfer requires conditional or lease-protected publication: Git, verified WebDAV, and R2/S3 with verified conditional writes are supported. The existing **Automatic sync** setting and shutdown policy are unchanged when this new opt-in is absent.
170
170
 
171
171
  Successful transfers show one summary. They do not reload resources, activate extension code, or rewrite model context. After local resources change, reload or restart when ready; newly pulled conversations still require explicit resume. Headless print/JSON skip this new startup feature. Reviewed directional settings/session-root transitions record both roots and verified settings evidence so interrupted apply can recover before or after settings installation. Older pending journals without transition evidence retain their original root-ownership checks and may require reviewed recovery; preserve their backups rather than downgrading. See [merge safety and recovery](./docs/merge-implementation-audit.md) for journal boundaries, external-writer limits, and downgrade guidance.
172
172
 
@@ -264,7 +264,7 @@ Review [Settings](#-settings) for included-content privacy and [Manager, conflic
264
264
  | --- | --- | --- | --- |
265
265
  | Git | Exact expected-ref lease | Existing SSH/configured credential helper | `<branch>:<storage.path>` |
266
266
  | WebDAV | Verified strong conditional requests | Private settings username/app password | `<url>/<storage.path>` |
267
- | R2/S3 | Read-check-write-verify | Private settings credentials | `<bucket>/<storage.path>` |
267
+ | R2/S3 | Verified strong conditional requests | Private settings credentials | `<bucket>/<storage.path>` |
268
268
 
269
269
  Git requires Git 2.30 or newer and a SHA-1-format remote repository.
270
270
  HTTPS userinfo, URL passwords, local paths, `file`, `git`, `ext`, and remote-helper transports are rejected.
@@ -275,8 +275,7 @@ WebDAV requires HTTPS except loopback tests.
275
275
  URL credentials, query strings, fragments, unsafe redirects, weak/missing ETags, and ignored conditional headers fail closed.
276
276
  `/sync doctor` verifies collection and conditional-write behavior with an isolated probe, then repairs a missing active-snapshot history entry.
277
277
 
278
- S3/R2 stages immutable bundles, rechecks the visible head before publication, and verifies afterward.
279
- Unlike Git/WebDAV, generic S3 does not provide an atomic compare-and-swap for `latest.json`; status review remains important for simultaneous writers.
278
+ S3/R2 publishes `latest.json` and history using ETag `If-Match`, or `If-None-Match: *` for first creation, then verifies the active pointer. Before each publication, an isolated `.pi-sync-probes/<random-id>` object verifies that the server rejects stale ETags and create-only writes to existing objects and rotates strong ETags. Credentials must allow read, write, and delete under the selected storage path; the probe is deleted even after cancellation. Missing/weak ETags, ignored conditions, and probe or cleanup failures stop publication. A failed cleanup reports the probe key for manual removal; bucket versioning may retain probe versions. `/sync doctor` remains read-only for S3/R2 and does not verify write support.
280
279
 
281
280
  Snapshot recovery is guarded: new journals record before/after images, older journals are retired only when their unchanged preimages are provable, and unrecognized bytes remain untouched for manual review. Current-session targets are never restored during startup. Preserve a blocked transaction and its backup, close Pi, and review the private evidence before restoring selected paths; malformed/unsupported journals are not disposable. Pi may have loaded resources before lifecycle recovery, so reload or restart explicitly after recovery if loaded resources need to match the restored files.
282
281
 
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  runCancellableOperation,
5
5
  showRemoteSelectionReview
6
- } from "./chunk-2CBF6SMT.ts";
6
+ } from "./chunk-TRCSEIAQ.ts";
7
7
  import {
8
8
  safeTerminalText
9
9
  } from "./chunk-AMJRUXFT.ts";
@@ -206,4 +206,4 @@ async function dispatchManagerResult(ctx, initialResult, origin, runRoute, signa
206
206
  export {
207
207
  dispatchManagerResult
208
208
  };
209
- //# sourceMappingURL=chunk-WKPX2SIC.ts.map
209
+ //# sourceMappingURL=chunk-FJGNDYGC.ts.map
@@ -25,7 +25,7 @@ import {
25
25
  } from "./chunk-V6VZ5TLG.ts";
26
26
  import {
27
27
  createSyncBackend
28
- } from "./chunk-QHSQCK7L.ts";
28
+ } from "./chunk-SDZCTCYQ.ts";
29
29
  import {
30
30
  applySnapshotTransaction,
31
31
  recoverPendingSnapshotTransactions,
@@ -68,7 +68,7 @@ import {
68
68
  import {
69
69
  SyncBackendConflictError,
70
70
  expectedRemoteHead
71
- } from "./chunk-UETBKTDI.ts";
71
+ } from "./chunk-YDVQYUHE.ts";
72
72
  import {
73
73
  SyncDecisionRequiredError
74
74
  } from "./chunk-7D5SF7P3.ts";
@@ -1017,6 +1017,68 @@ async function applyMergedSnapshot(before, after, protectedPaths, options, valid
1017
1017
  await acquire(0);
1018
1018
  }
1019
1019
 
1020
+ // src/sync/merge-transfer-error.ts
1021
+ var MAX_CAUSES = 4;
1022
+ var MAX_DETAIL_LENGTH = 600;
1023
+ function mergeTransferError(error, backend) {
1024
+ const messages = [];
1025
+ const seen = /* @__PURE__ */ new Set();
1026
+ let current = error;
1027
+ while (current instanceof Error && !seen.has(current) && seen.size < MAX_CAUSES) {
1028
+ seen.add(current);
1029
+ const code = current.code;
1030
+ const status = current.status;
1031
+ const metadata = [
1032
+ typeof code === "string" && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : void 0,
1033
+ typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? `HTTP ${status}` : void 0
1034
+ ].filter(Boolean);
1035
+ const message = `${current.message}${metadata.length ? ` (${metadata.join(", ")})` : ""}`;
1036
+ if (message && !messages.includes(message)) messages.push(message);
1037
+ current = current.cause;
1038
+ }
1039
+ let detail = messages.join(" \u2192 ") || "No error details available.";
1040
+ for (const secret of credentialVariants(backend)) detail = detail.replaceAll(secret, "[REDACTED]");
1041
+ detail = detail.replace(/\b(Bearer|Basic)\s+[^\s<>"']+/giu, "$1 [REDACTED]");
1042
+ detail = safeTerminalText(detail);
1043
+ if (detail.length > MAX_DETAIL_LENGTH) detail = `${detail.slice(0, MAX_DETAIL_LENGTH)}\u2026 (details truncated)`;
1044
+ return new Error(
1045
+ `Merged transfer interrupted; journal and backup retained. Run /sync sync to reconcile before further mutations. Transfer failure: ${detail}`,
1046
+ { cause: error }
1047
+ );
1048
+ }
1049
+ function credentialVariants(backend) {
1050
+ const secrets = [];
1051
+ let address;
1052
+ switch (backend.type) {
1053
+ case "s3":
1054
+ secrets.push(backend.profile.accessKeyId, backend.profile.secretAccessKey, backend.profile.sessionToken ?? "");
1055
+ address = backend.profile.endpoint;
1056
+ break;
1057
+ case "webdav":
1058
+ secrets.push(
1059
+ backend.profile.username,
1060
+ backend.profile.password,
1061
+ Buffer.from(`${backend.profile.username}:${backend.profile.password}`).toString("base64")
1062
+ );
1063
+ address = backend.profile.url;
1064
+ break;
1065
+ case "git":
1066
+ address = backend.profile.remote;
1067
+ break;
1068
+ }
1069
+ try {
1070
+ const url = new URL(address);
1071
+ secrets.push(url.username, url.password, ...url.searchParams.values());
1072
+ for (const value of [url.username, url.password]) secrets.push(decodeURIComponent(value));
1073
+ } catch {
1074
+ }
1075
+ return [
1076
+ ...new Set(
1077
+ secrets.filter(Boolean).flatMap((value) => [value, encodeURIComponent(value), JSON.stringify(value).slice(1, -1)])
1078
+ )
1079
+ ].sort((left, right) => right.length - left.length);
1080
+ }
1081
+
1020
1082
  // src/sync/partial-progress.ts
1021
1083
  function acceptedMergeFiles(decisions, localFiles, withheld) {
1022
1084
  const localByPath = new Map(localFiles.map((file) => [file.path, file]));
@@ -1559,10 +1621,7 @@ An explicit direction adopts this setup's included-content policy; no automatic
1559
1621
  if (attempt + 1 < MAX_ATTEMPTS) continue;
1560
1622
  throw new Error("Remote changed before commit in all three attempts; retry later.", { cause: error });
1561
1623
  }
1562
- throw new Error(
1563
- "Merged transfer interrupted; journal and backup retained. Run /sync sync to reconcile before further mutations.",
1564
- { cause: error }
1565
- );
1624
+ throw mergeTransferError(error, config.backend);
1566
1625
  }
1567
1626
  const completed = await completeJournal(ctx, config, backend, journal, validate, options.signal, options.auto);
1568
1627
  if (!completed) {
@@ -1605,7 +1664,7 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
1605
1664
  }
1606
1665
  const snapshotOptions = { ...snapshotOptionsForContext(ctx, config), sessionDir: sessionRoot, signal };
1607
1666
  const head = await backend.readHead(signal);
1608
- if (!journal.committedHead && head && backend.sameRevision(head.revision, journal.expectedHead.revision)) {
1667
+ if (!journal.committedHead && head && (backend.sameRevision(head.revision, journal.expectedHead.revision) || backend.matchesUncommittedRecoveryHead?.(head, journal.expectedHead))) {
1609
1668
  await validate();
1610
1669
  const local = await createSnapshot(config.snapshotIdentity, snapshotOptions);
1611
1670
  if (!sameHashes(fileHashMap(local), fileHashMap(journal.before)) || syncStateFingerprint(await readStateForConfig(config)) !== journal.stateIdentity) {
@@ -1743,4 +1802,4 @@ export {
1743
1802
  createSyncDecision,
1744
1803
  mergeSync
1745
1804
  };
1746
- //# sourceMappingURL=chunk-NLS2NV3K.ts.map
1805
+ //# sourceMappingURL=chunk-QDPED3XJ.ts.map