@narumitw/pi-sync 0.54.3 → 0.57.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.
- package/README.md +38 -11
- package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
- package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
- package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
- package/dist/chunks/{chunk-GC3QOL6J.ts → chunk-AAEEHR7H.ts} +31 -87
- package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
- package/dist/chunks/{chunk-R35N4CL3.ts → chunk-B4QZBDBT.ts} +2 -2
- package/dist/chunks/{chunk-KGE47GCZ.ts → chunk-CRKTAPSL.ts} +8 -8
- package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
- package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
- package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
- package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
- package/dist/chunks/{chunk-HMXUKU43.ts → chunk-EUMINX7L.ts} +6 -6
- package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
- package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
- package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
- package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
- package/dist/chunks/{chunk-YTHQEW35.ts → chunk-GP5DU62N.ts} +8 -9
- package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
- package/dist/chunks/{chunk-AAZDBIBJ.ts → chunk-GSNPASKB.ts} +2 -2
- package/dist/chunks/{chunk-E6ONFG57.ts → chunk-IWZH5KYS.ts} +11 -8
- package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
- package/dist/chunks/{chunk-CAGJOKB7.ts → chunk-JJTARUSO.ts} +15 -14
- package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
- package/dist/chunks/{chunk-ITFR6NNQ.ts → chunk-NLWS55B6.ts} +2 -1
- package/dist/chunks/{chunk-ITFR6NNQ.ts.map → chunk-NLWS55B6.ts.map} +2 -2
- package/dist/chunks/{chunk-2FETCGGT.ts → chunk-OEWQVWLC.ts} +4 -4
- package/dist/chunks/{chunk-35GM6XKM.ts → chunk-OTZRCKYP.ts} +128 -382
- package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
- package/dist/chunks/{chunk-XU2VXGQV.ts → chunk-OWBAZA6J.ts} +14 -8
- package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
- package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
- package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
- package/dist/chunks/chunk-QGK445K7.ts +21 -0
- package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
- package/dist/chunks/{chunk-KATKRR3D.ts → chunk-QO333NM4.ts} +32 -11
- package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
- package/dist/chunks/{chunk-I4JYVFUS.ts → chunk-QSHMOL3I.ts} +21 -22
- package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
- package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
- package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
- package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
- package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
- package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
- package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
- package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
- package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
- package/dist/chunks/{chunk-VCL7TZYW.ts → chunk-UPNVIUBN.ts} +3 -3
- package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
- package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
- package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
- package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
- package/dist/chunks/{chunk-A3TFXY4O.ts → chunk-YK52VDXG.ts} +6 -5
- package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
- package/dist/chunks/config-KGIYTTXQ.ts +37 -0
- package/dist/chunks/{conflict-artifacts-4CZFB7WI.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
- package/dist/chunks/{conflict-review-ZOTOHL2E.ts → conflict-review-6WIEQLXW.ts} +31 -25
- package/dist/chunks/{conflict-review-ZOTOHL2E.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
- package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
- package/dist/chunks/{file-selection-UNHBVVON.ts → file-selection-3JJYOVNR.ts} +5 -5
- package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
- package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
- package/dist/chunks/{manager-result-dispatcher-HVRTPEVK.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
- package/dist/chunks/{manager-ui-ZIN4BUSF.ts → manager-ui-KRAW3NE2.ts} +184 -45
- package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-ICXVTCML.ts → merge-baseline-store-RFCAH7KL.ts} +5 -5
- package/dist/chunks/{remote-selection-ui-H6AEVA3Q.ts → remote-selection-ui-5725JDNC.ts} +9 -8
- package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
- package/dist/chunks/{s3-backend-Y4P3O5B5.ts → s3-backend-Y7WSCLJI.ts} +38 -38
- package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
- package/dist/chunks/{setup-switch-ZMW5C4M7.ts → setup-switch-E7PHKI6J.ts} +7 -6
- package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
- package/dist/chunks/{setup-wizard-VW7F3JN4.ts → setup-wizard-65DROCIJ.ts} +14 -13
- package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
- package/dist/chunks/{sync-inspection-3EMS2J5B.ts → sync-inspection-XFJOAMJD.ts} +8 -8
- package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
- package/dist/chunks/{sync-operations-2CWST333.ts → sync-operations-HDUDT5PD.ts} +62 -63
- package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
- package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
- package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
- package/dist/index.ts +165 -85
- package/dist/index.ts.map +3 -3
- package/docs/agent-mode-verification.md +99 -0
- package/docs/continuous-sync-verification.md +87 -0
- package/docs/settings.md +32 -247
- package/package.json +11 -5
- package/skills/configure-pi-sync/SKILL.md +54 -0
- package/skills/configure-pi-sync/references/configuration.md +650 -0
- package/src/backends/git/git-backend.ts +1 -4
- package/src/backends/git/git-runner.ts +1 -5
- package/src/backends/s3/s3-backend.ts +3 -25
- package/src/backends/s3/s3-client.ts +14 -2
- package/src/backends/snapshot-pointer.ts +27 -0
- package/src/backends/webdav/webdav-backend.ts +3 -29
- package/src/backends/webdav/webdav-client.ts +11 -7
- package/src/commands/command-execution.ts +5 -2
- package/src/commands/command-handler.ts +15 -3
- package/src/commands/command-types.ts +6 -0
- package/src/commands/command.ts +1 -0
- package/src/settings/config.ts +7 -0
- package/src/settings/settings-store.ts +0 -34
- package/src/settings/settings-types.ts +8 -0
- package/src/settings/settings-validation.ts +1 -1
- package/src/snapshot/snapshot-codec.ts +1 -5
- package/src/state/lock.ts +13 -10
- package/src/state/merge-baseline-store.ts +2 -2
- package/src/state/state-directory.ts +8 -1
- package/src/sync/agent-candidate.ts +251 -0
- package/src/sync/agent-plan.ts +87 -0
- package/src/sync/agent-policy.ts +13 -0
- package/src/sync/agent-request.ts +17 -0
- package/src/sync/agent-runner.ts +188 -0
- package/src/sync/agent-store.ts +151 -0
- package/src/sync/agent-sync.ts +91 -0
- package/src/sync/automatic-sync.ts +2 -1
- package/src/sync/continuous-errors.ts +85 -0
- package/src/sync/continuous-lease.ts +221 -0
- package/src/sync/continuous-record.ts +179 -0
- package/src/sync/continuous-storage.ts +55 -0
- package/src/sync/continuous-sync.ts +516 -0
- package/src/sync/continuous-watch.ts +59 -0
- package/src/sync/merged-sync.ts +70 -12
- package/src/sync/remote-snapshot.ts +2 -3
- package/src/sync/settings-merge.ts +19 -3
- package/src/sync/setup-switch.ts +1 -5
- package/src/sync/startup-check.ts +15 -11
- package/src/sync/sync-inspection.ts +8 -1
- package/src/sync/sync-mutations.ts +6 -17
- package/src/sync/sync-queries.ts +5 -0
- package/src/sync/text-merge.ts +3 -3
- package/src/sync-extension.ts +75 -19
- package/src/ui/agent-mode-setting.ts +91 -0
- package/src/ui/manager-state.ts +2 -0
- package/src/ui/remote-selection-ui.ts +5 -5
- package/src/ui/settings-ui.ts +83 -1
- package/src/ui/setup/s3-credentials-ui.ts +1 -5
- package/src/ui/setup/setup-name-ui.ts +2 -5
- package/src/ui/setup/setup-switcher.ts +2 -0
- package/src/ui/sync-setups-ui.ts +3 -5
- package/dist/chunks/chunk-26PMXY7E.ts +0 -362
- package/dist/chunks/chunk-26PMXY7E.ts.map +0 -7
- package/dist/chunks/chunk-35GM6XKM.ts.map +0 -7
- package/dist/chunks/chunk-A3TFXY4O.ts.map +0 -7
- package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
- package/dist/chunks/chunk-CAGJOKB7.ts.map +0 -7
- package/dist/chunks/chunk-E6ONFG57.ts.map +0 -7
- package/dist/chunks/chunk-GC3QOL6J.ts.map +0 -7
- package/dist/chunks/chunk-I4JYVFUS.ts.map +0 -7
- package/dist/chunks/chunk-KATKRR3D.ts.map +0 -7
- package/dist/chunks/chunk-XU2VXGQV.ts.map +0 -7
- package/dist/chunks/chunk-YTHQEW35.ts.map +0 -7
- package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
- package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
- package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +0 -7
- package/dist/chunks/s3-backend-Y4P3O5B5.ts.map +0 -7
- package/dist/chunks/setup-wizard-VW7F3JN4.ts.map +0 -7
- package/dist/chunks/sync-operations-2CWST333.ts.map +0 -7
- package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
- /package/dist/chunks/{conflict-artifacts-4CZFB7WI.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
- /package/dist/chunks/{chunk-R35N4CL3.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
- /package/dist/chunks/{chunk-KGE47GCZ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
- /package/dist/chunks/{chunk-HMXUKU43.ts.map → chunk-EUMINX7L.ts.map} +0 -0
- /package/dist/chunks/{chunk-AAZDBIBJ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
- /package/dist/chunks/{chunk-2FETCGGT.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
- /package/dist/chunks/{chunk-VCL7TZYW.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-HVRTPEVK.ts.map → config-KGIYTTXQ.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-ICXVTCML.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-H6AEVA3Q.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
- /package/dist/chunks/{file-selection-UNHBVVON.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-ZMW5C4M7.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-3EMS2J5B.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
package/src/sync/merged-sync.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { confirmMergeReview } from "../ui/merge-review.js";
|
|
38
38
|
import { mergedSyncSummary } from "../ui/merged-sync-summary.js";
|
|
39
39
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
40
|
+
import { startupTransferEnabled } from "./agent-policy.js";
|
|
40
41
|
import { releaseConflictArtifacts } from "./conflict-artifact-ownership.js";
|
|
41
42
|
import {
|
|
42
43
|
type CreatedConflictArtifact,
|
|
@@ -82,7 +83,11 @@ export async function mergeSync(
|
|
|
82
83
|
resolution?: ConflictResolution,
|
|
83
84
|
) {
|
|
84
85
|
const timings = options.startupTimings ?? createStartupTimings();
|
|
85
|
-
const
|
|
86
|
+
const sessionOwner = captureMutationOwner(ctx, options.signal);
|
|
87
|
+
const validateOwner = (config?: AnySyncConfig) => {
|
|
88
|
+
sessionOwner();
|
|
89
|
+
options.background?.validate(config);
|
|
90
|
+
};
|
|
86
91
|
const isCurrent = () => {
|
|
87
92
|
try {
|
|
88
93
|
validateOwner();
|
|
@@ -92,7 +97,9 @@ export async function mergeSync(
|
|
|
92
97
|
}
|
|
93
98
|
};
|
|
94
99
|
const config = await loadConfig(options.setup);
|
|
100
|
+
validateOwner(config);
|
|
95
101
|
const configToken = syncCheckConfigFingerprint(config);
|
|
102
|
+
const modelToken = JSON.stringify(ctx.model);
|
|
96
103
|
const sessionRoot = config.include.includes("sessions") ? await effectiveSessionRoot(ctx) : undefined;
|
|
97
104
|
validateOwner();
|
|
98
105
|
const snapshotOptions = {
|
|
@@ -104,7 +111,7 @@ export async function mergeSync(
|
|
|
104
111
|
timings.measure("transfer authorization", async () => {
|
|
105
112
|
validateOwner();
|
|
106
113
|
const latest = await loadConfig(options.setup);
|
|
107
|
-
validateOwner();
|
|
114
|
+
validateOwner(latest);
|
|
108
115
|
if (configToken !== syncCheckConfigFingerprint(latest))
|
|
109
116
|
throw new Error("Sync setup or settings changed during transfer; reopen sync.");
|
|
110
117
|
if (sessionRoot !== undefined) {
|
|
@@ -113,22 +120,29 @@ export async function mergeSync(
|
|
|
113
120
|
if (currentRoot !== sessionRoot)
|
|
114
121
|
throw new Error("Session storage root changed during transfer; evidence retained for review.");
|
|
115
122
|
}
|
|
116
|
-
if (
|
|
123
|
+
if (config.agentMode && JSON.stringify(ctx.model) !== modelToken)
|
|
124
|
+
throw new Error("Agent mode paused: selected model changed during transfer.");
|
|
125
|
+
if (options.auto && (!startupTransferEnabled(latest) || !ctx.isIdle()))
|
|
117
126
|
throw new Error("Automatic transfer is no longer authorized at this idle boundary.");
|
|
118
127
|
});
|
|
119
128
|
await validate();
|
|
120
129
|
const backend = await timings.measure("transfer backend initialization", () => factory(config));
|
|
121
130
|
await validate();
|
|
122
|
-
if (options.auto && backend.capability === "read-check-write-verify") {
|
|
123
|
-
throw new Error(
|
|
124
|
-
"Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Use manual sync.",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
131
|
const pending = await timings.measure("transfer journal read", () => readMergeJournal(config));
|
|
128
132
|
await validate();
|
|
129
133
|
if (pending) {
|
|
130
134
|
const recovered = await timings.measure("transfer journal recovery", () =>
|
|
131
|
-
completeJournal(
|
|
135
|
+
completeJournal(
|
|
136
|
+
ctx,
|
|
137
|
+
config,
|
|
138
|
+
backend,
|
|
139
|
+
pending,
|
|
140
|
+
validate,
|
|
141
|
+
timings,
|
|
142
|
+
options.signal,
|
|
143
|
+
options.auto,
|
|
144
|
+
options.background?.validate,
|
|
145
|
+
),
|
|
132
146
|
);
|
|
133
147
|
if (!recovered) {
|
|
134
148
|
if ((options.auto || !options.silent) && !options.signal?.aborted)
|
|
@@ -142,6 +156,12 @@ export async function mergeSync(
|
|
|
142
156
|
ctx.ui.notify("Recovered the committed merged transfer. Run sync again to inspect newer changes.", "info");
|
|
143
157
|
return "applied" as const;
|
|
144
158
|
}
|
|
159
|
+
// Recovery above never publishes. This gate applies only to new autonomous transfers.
|
|
160
|
+
if ((options.auto || config.agentMode) && backend.capability === "read-check-write-verify") {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Disable Agent mode and use manual sync.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
145
165
|
const textBudget = { remaining: TEXT_MERGE_WORK_LIMIT };
|
|
146
166
|
const createdArtifacts: CreatedConflictArtifact[] = [];
|
|
147
167
|
const ownedStatePath = statePathForConfig(config);
|
|
@@ -300,7 +320,33 @@ export async function mergeSync(
|
|
|
300
320
|
);
|
|
301
321
|
await validate();
|
|
302
322
|
}
|
|
303
|
-
if (
|
|
323
|
+
if (config.agentMode && !resolution) {
|
|
324
|
+
const { resolveAgentPlan } = await import("./agent-plan.js");
|
|
325
|
+
await validate();
|
|
326
|
+
plan = await resolveAgentPlan(
|
|
327
|
+
ctx,
|
|
328
|
+
options,
|
|
329
|
+
config,
|
|
330
|
+
backend,
|
|
331
|
+
state,
|
|
332
|
+
local,
|
|
333
|
+
localRaw,
|
|
334
|
+
remote,
|
|
335
|
+
head,
|
|
336
|
+
plan,
|
|
337
|
+
snapshotOptions,
|
|
338
|
+
validate,
|
|
339
|
+
);
|
|
340
|
+
await validate();
|
|
341
|
+
if (plan.kind !== "planned" || (plan.conflicts.length && !config.partialSync))
|
|
342
|
+
throw new Error(
|
|
343
|
+
"Agent mode paused: unsupported or refused conflict groups remain; no transfer was performed.",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
plan.kind !== "planned" ||
|
|
348
|
+
(plan.conflicts.length && (!config.partialSync || (options.auto && !config.agentMode)))
|
|
349
|
+
) {
|
|
304
350
|
const decision = review(
|
|
305
351
|
selectionCompatible
|
|
306
352
|
? `Conflicting or protected paths require review; no merged transfer was performed. Use /sync diff and an explicit direction.${resolved.fields.length ? ` Settings fields: ${resolved.fields.map(safeTerminalText).join(", ")}` : ""}`
|
|
@@ -467,7 +513,7 @@ export async function mergeSync(
|
|
|
467
513
|
await validate();
|
|
468
514
|
if (!config.skipSecretScan && scanSnapshot(upload).length)
|
|
469
515
|
throw new Error("Refusing to merge possible secrets. Review managed content before syncing.");
|
|
470
|
-
if (!options.yes) {
|
|
516
|
+
if (!options.yes && !config.agentMode) {
|
|
471
517
|
const summary = mergedSyncSummary({
|
|
472
518
|
setupName: config.setupName,
|
|
473
519
|
destination: backend.destination,
|
|
@@ -585,7 +631,17 @@ export async function mergeSync(
|
|
|
585
631
|
throw mergeTransferError(error, config.backend);
|
|
586
632
|
}
|
|
587
633
|
const completed = await timings.measure("transfer journal recovery", () =>
|
|
588
|
-
completeJournal(
|
|
634
|
+
completeJournal(
|
|
635
|
+
ctx,
|
|
636
|
+
config,
|
|
637
|
+
backend,
|
|
638
|
+
journal,
|
|
639
|
+
validate,
|
|
640
|
+
timings,
|
|
641
|
+
options.signal,
|
|
642
|
+
options.auto,
|
|
643
|
+
options.background?.validate,
|
|
644
|
+
),
|
|
589
645
|
);
|
|
590
646
|
if (!completed) {
|
|
591
647
|
if (!options.signal?.aborted && (options.auto || !options.silent))
|
|
@@ -624,6 +680,7 @@ async function completeJournal(
|
|
|
624
680
|
timings: StartupTimings,
|
|
625
681
|
signal?: AbortSignal,
|
|
626
682
|
auto = false,
|
|
683
|
+
validateBackground?: () => void,
|
|
627
684
|
) {
|
|
628
685
|
if (
|
|
629
686
|
journal.identity !== mergeJournalIdentity(config, backend.identity) ||
|
|
@@ -734,6 +791,7 @@ async function completeJournal(
|
|
|
734
791
|
const validateOwner = captureMutationOwner(ctx, signal);
|
|
735
792
|
const validateMutation = () => {
|
|
736
793
|
validateOwner();
|
|
794
|
+
validateBackground?.();
|
|
737
795
|
if (auto && !ctx.isIdle()) throw new Error("Automatic file apply is no longer idle; evidence retained.");
|
|
738
796
|
};
|
|
739
797
|
await timings.measure("transfer local apply", () =>
|
|
@@ -5,6 +5,7 @@ import { filterSnapshotForConfigPolicy } from "../snapshot/snapshot.js";
|
|
|
5
5
|
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
6
6
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
7
7
|
import { sameLocalFields, validatePortableSnapshot } from "./local-fields.js";
|
|
8
|
+
import { throwIfAborted } from "./signals.js";
|
|
8
9
|
import {
|
|
9
10
|
inspectRemoteSelection,
|
|
10
11
|
type RemoteSelectionState,
|
|
@@ -15,9 +16,7 @@ import {
|
|
|
15
16
|
|
|
16
17
|
export async function readSnapshotForHead(backend: SyncBackend, head: RemoteHead, signal?: AbortSignal) {
|
|
17
18
|
const snapshot = await backend.readSnapshot(head.snapshotRef, signal);
|
|
18
|
-
|
|
19
|
-
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
20
|
-
}
|
|
19
|
+
throwIfAborted(signal);
|
|
21
20
|
validatePortableSnapshot(snapshot);
|
|
22
21
|
if (snapshot.id !== head.snapshotId) {
|
|
23
22
|
throw new Error(`Remote head ${head.snapshotId} resolved to unexpected snapshot ${snapshot.id}.`);
|
|
@@ -10,7 +10,7 @@ export type SettingsMergeResult =
|
|
|
10
10
|
| { kind: "merged"; content: Buffer }
|
|
11
11
|
| { kind: "review"; reason: "unsupported-format" | "field-conflict"; fields: string[] };
|
|
12
12
|
|
|
13
|
-
const COUPLED_FIELDS = [
|
|
13
|
+
export const COUPLED_FIELDS = [
|
|
14
14
|
["defaultProvider", "defaultModel"],
|
|
15
15
|
["queueMode", "steeringMode"],
|
|
16
16
|
["websockets", "transport"],
|
|
@@ -19,7 +19,12 @@ const COUPLED_FIELDS = [
|
|
|
19
19
|
] as const;
|
|
20
20
|
|
|
21
21
|
/** Only global settings.json; objects/arrays are atomic units until their invariants are proven. */
|
|
22
|
-
export function mergeSettingsJson(
|
|
22
|
+
export function mergeSettingsJson(
|
|
23
|
+
ancestor: Buffer,
|
|
24
|
+
local: Buffer,
|
|
25
|
+
remote: Buffer,
|
|
26
|
+
selections: ReadonlyMap<string, "local" | "remote"> = new Map(),
|
|
27
|
+
): SettingsMergeResult {
|
|
23
28
|
let documents: JsonDocument[];
|
|
24
29
|
try {
|
|
25
30
|
documents = [ancestor, local, remote].map(parseSettingsDocument);
|
|
@@ -38,7 +43,18 @@ export function mergeSettingsJson(ancestor: Buffer, local: Buffer, remote: Buffe
|
|
|
38
43
|
unit.every((field) =>
|
|
39
44
|
jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value),
|
|
40
45
|
);
|
|
41
|
-
const
|
|
46
|
+
const choice = selections.get(unit[0] ?? "");
|
|
47
|
+
const source = equal(left, right)
|
|
48
|
+
? left
|
|
49
|
+
: equal(left, base)
|
|
50
|
+
? right
|
|
51
|
+
: equal(right, base)
|
|
52
|
+
? left
|
|
53
|
+
: choice === "local"
|
|
54
|
+
? left
|
|
55
|
+
: choice === "remote"
|
|
56
|
+
? right
|
|
57
|
+
: undefined;
|
|
42
58
|
if (!source) {
|
|
43
59
|
conflicts.push(...unit.filter((field) => fields.has(field)));
|
|
44
60
|
continue;
|
package/src/sync/setup-switch.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { configuredSyncSetupNames, updateLocalConfig } from "../settings/setting
|
|
|
5
5
|
import type { OnSwitchAction } from "../settings/settings-types.js";
|
|
6
6
|
import { normalizeOnSwitch } from "../settings/settings-validation.js";
|
|
7
7
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
8
|
+
import { throwIfAborted } from "./signals.js";
|
|
8
9
|
import { SetupPullRequiresUiError } from "./sync-errors.js";
|
|
9
10
|
|
|
10
11
|
export { SetupPullRequiresUiError } from "./sync-errors.js";
|
|
@@ -128,8 +129,3 @@ export async function useSyncSetup(
|
|
|
128
129
|
}
|
|
129
130
|
return { pullApplied: pullOutcome === "applied" };
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
-
function throwIfAborted(signal?: AbortSignal) {
|
|
133
|
-
if (!signal?.aborted) return;
|
|
134
|
-
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
135
|
-
}
|
|
@@ -9,6 +9,8 @@ import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-st
|
|
|
9
9
|
import type { SyncAttentionController } from "../ui/sync-attention.js";
|
|
10
10
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
11
11
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
12
|
+
import { startupTransferEnabled } from "./agent-policy.js";
|
|
13
|
+
import { withAgentSyncLock } from "./agent-sync.js";
|
|
12
14
|
import { combineSignals, throwIfAborted } from "./signals.js";
|
|
13
15
|
import { createStartupTimings } from "./startup-timings.js";
|
|
14
16
|
import { errorMessage } from "./sync-errors.js";
|
|
@@ -42,7 +44,7 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
42
44
|
},
|
|
43
45
|
start(ctx: ExtensionContext, sessionSignal: AbortSignal, initialConfig?: AnySyncConfig) {
|
|
44
46
|
if (!ctx.hasUI || sessionSignal.aborted) return;
|
|
45
|
-
if (initialConfig
|
|
47
|
+
if (initialConfig && startupTransferEnabled(initialConfig) && !ctx.isIdle()) {
|
|
46
48
|
pending = { ctx, signal: sessionSignal, config: initialConfig };
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
@@ -54,7 +56,7 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
54
56
|
controller,
|
|
55
57
|
owner: ctx.sessionManager,
|
|
56
58
|
promise: Promise.resolve(),
|
|
57
|
-
transfer: Boolean(initialConfig
|
|
59
|
+
transfer: Boolean(initialConfig && startupTransferEnabled(initialConfig)),
|
|
58
60
|
};
|
|
59
61
|
active = task;
|
|
60
62
|
const isCurrent = () => !sessionSignal.aborted && active === task && task.owner === ctx.sessionManager;
|
|
@@ -71,9 +73,9 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
71
73
|
try {
|
|
72
74
|
throwIfAborted(signal);
|
|
73
75
|
config = initialConfig ?? (await loadConfigForCheck());
|
|
74
|
-
task.transfer =
|
|
76
|
+
task.transfer = startupTransferEnabled(config);
|
|
75
77
|
throwIfAborted(signal);
|
|
76
|
-
if (!config.automatic && !config
|
|
78
|
+
if (!config.automatic && !startupTransferEnabled(config)) return;
|
|
77
79
|
const captured = config;
|
|
78
80
|
const identity = syncCheckConfigFingerprint(captured);
|
|
79
81
|
checking = true;
|
|
@@ -106,10 +108,10 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
106
108
|
configIdentity: identity,
|
|
107
109
|
checkedAt: new Date().toISOString(),
|
|
108
110
|
inspection,
|
|
109
|
-
automaticTransfer: captured
|
|
111
|
+
automaticTransfer: startupTransferEnabled(captured),
|
|
110
112
|
});
|
|
111
113
|
if (
|
|
112
|
-
captured
|
|
114
|
+
startupTransferEnabled(captured) &&
|
|
113
115
|
!inspection.firstSync &&
|
|
114
116
|
inspection.head &&
|
|
115
117
|
inspection.selectionState?.kind === "same"
|
|
@@ -123,8 +125,9 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
123
125
|
}
|
|
124
126
|
await timings.measure("automatic transfer", () =>
|
|
125
127
|
withStateDirectoryAccess(() =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
withAgentSyncLock(
|
|
129
|
+
ctx,
|
|
130
|
+
{
|
|
128
131
|
args: [],
|
|
129
132
|
yes: true,
|
|
130
133
|
force: false,
|
|
@@ -135,7 +138,8 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
135
138
|
signal,
|
|
136
139
|
startupTimings: timings,
|
|
137
140
|
onCommit: () => attention.clearObservation(),
|
|
138
|
-
}
|
|
141
|
+
},
|
|
142
|
+
(options) => operations.syncBoth(ctx, options),
|
|
139
143
|
),
|
|
140
144
|
),
|
|
141
145
|
);
|
|
@@ -149,7 +153,7 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
149
153
|
configIdentity: identity,
|
|
150
154
|
checkedAt: new Date().toISOString(),
|
|
151
155
|
inspection,
|
|
152
|
-
automaticTransfer: captured
|
|
156
|
+
automaticTransfer: startupTransferEnabled(captured),
|
|
153
157
|
});
|
|
154
158
|
if (ctx.mode === "rpc") attention.notifyObservation(ctx);
|
|
155
159
|
} catch (error) {
|
|
@@ -163,7 +167,7 @@ export function createStartupCheck(loaders: SyncLoaders, attention: SyncAttentio
|
|
|
163
167
|
if (!latest || syncCheckConfigFingerprint(latest) !== syncCheckConfigFingerprint(config)) return;
|
|
164
168
|
}
|
|
165
169
|
ctx.ui.notify(
|
|
166
|
-
config
|
|
170
|
+
config && startupTransferEnabled(config)
|
|
167
171
|
? `pi-sync automatic transfer stopped: ${safeTerminalText(errorMessage(error))}. Run /sync to review or reconcile retained recovery evidence. No resources were reloaded.`
|
|
168
172
|
: `pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}. Run /sync status to retry. No startup transfer was performed.`,
|
|
169
173
|
"warning",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
|
|
2
3
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
3
4
|
import { createSnapshot } from "../snapshot/snapshot.js";
|
|
@@ -6,7 +7,7 @@ import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-st
|
|
|
6
7
|
import { throwIfAborted } from "./signals.js";
|
|
7
8
|
import { createStartupTimings, type StartupTimings } from "./startup-timings.js";
|
|
8
9
|
import { inspectRemoteSelection } from "./sync-policy.js";
|
|
9
|
-
import { hasLocalChanges, remoteChangedSinceState } from "./sync-state.js";
|
|
10
|
+
import { fileHashMap, hasLocalChanges, remoteChangedSinceState } from "./sync-state.js";
|
|
10
11
|
|
|
11
12
|
/** Advisory baseline-relative observation, never an authorization to transfer files. */
|
|
12
13
|
export async function inspectSync(
|
|
@@ -27,7 +28,13 @@ export async function inspectSync(
|
|
|
27
28
|
throwIfAborted(signal);
|
|
28
29
|
const head = await timings.measure("remote head", () => backend.readHead(signal));
|
|
29
30
|
throwIfAborted(signal);
|
|
31
|
+
const freshness: { localIdentity?: string } = {
|
|
32
|
+
localIdentity: createHash("sha256")
|
|
33
|
+
.update(JSON.stringify(fileHashMap(local)))
|
|
34
|
+
.digest("hex"),
|
|
35
|
+
};
|
|
30
36
|
return {
|
|
37
|
+
...freshness,
|
|
31
38
|
head,
|
|
32
39
|
selectionState: head ? inspectRemoteSelection(config.include, { selection: head.selection, files: [] }) : undefined,
|
|
33
40
|
localFiles: local.files.length,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "../ui/sync-format.js";
|
|
33
33
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
34
34
|
import { confirmTransferReview } from "../ui/transfer-review.js";
|
|
35
|
+
import { startupTransferEnabled } from "./agent-policy.js";
|
|
35
36
|
import { inspectCaseCleanup, revalidateCaseCleanup } from "./apply-review.js";
|
|
36
37
|
import { confirmFieldMigration } from "./field-migration.js";
|
|
37
38
|
import { overlayLocalFields, portableSnapshot, sameLocalFields, validatePortableSnapshot } from "./local-fields.js";
|
|
@@ -429,7 +430,9 @@ export async function syncBoth(
|
|
|
429
430
|
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
430
431
|
const config = await loadConfig(options.setup);
|
|
431
432
|
throwIfAborted(options.signal);
|
|
432
|
-
|
|
433
|
+
options.background?.validate(config);
|
|
434
|
+
if (config.include.length > 0 && options.auto && startupTransferEnabled(config))
|
|
435
|
+
return mergeSync(ctx, options, factory);
|
|
433
436
|
if (!options.auto && config.include.length > 0 && (await readMergeJournal(config)))
|
|
434
437
|
return mergeSync(ctx, options, factory);
|
|
435
438
|
const state = await readStateForConfig(config);
|
|
@@ -451,15 +454,13 @@ export async function syncBoth(
|
|
|
451
454
|
throwIfAborted(options.signal);
|
|
452
455
|
const localChanged = hasLocalChanges(local, state, config);
|
|
453
456
|
const remoteChanged = remote ? hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx)) : false;
|
|
454
|
-
const firstSync = !state.lastAppliedSnapshot;
|
|
455
457
|
if (options.auto) throw new Error("Automatic transfer is not authorized by the current settings.");
|
|
456
458
|
|
|
459
|
+
// Established baselines return through mergeSync above; only initial-source selection remains.
|
|
457
460
|
const portableLocal = portableSnapshot(local, config.localFields);
|
|
458
461
|
const acceptMatchingState = async (snapshot: Snapshot) => {
|
|
459
462
|
requireCompatibleRemoteSelection(config, snapshot);
|
|
460
463
|
const accepted = portableSnapshot(snapshot, config.localFields);
|
|
461
|
-
if (state.lastAppliedSnapshot && !sameLocalFields(state.localFields, config.localFields))
|
|
462
|
-
throw new Error("Local-field rules changed; review and confirm a directional migration first.");
|
|
463
464
|
await writeStateForConfig(
|
|
464
465
|
config,
|
|
465
466
|
{
|
|
@@ -477,7 +478,7 @@ export async function syncBoth(
|
|
|
477
478
|
await pruneMergeBaselines(config, await readStateForConfig(config), validateMutation);
|
|
478
479
|
validateMutation();
|
|
479
480
|
};
|
|
480
|
-
if (
|
|
481
|
+
if (remote && remote.files.length > 0 && local.files.length > 0) {
|
|
481
482
|
requireCompatibleRemoteSelection(config, remote);
|
|
482
483
|
const portableRemote = portableSnapshot(remote, config.localFields);
|
|
483
484
|
if (!canPullRemoteSettingsOnFirstSync(portableLocal, portableRemote)) {
|
|
@@ -525,18 +526,6 @@ export async function syncBoth(
|
|
|
525
526
|
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
526
527
|
return;
|
|
527
528
|
}
|
|
528
|
-
if (localChanged && remoteChanged && state.lastAppliedSnapshot) {
|
|
529
|
-
throw createSyncDecision({
|
|
530
|
-
kind: "both-changed",
|
|
531
|
-
config,
|
|
532
|
-
state,
|
|
533
|
-
local,
|
|
534
|
-
remote,
|
|
535
|
-
localChanged,
|
|
536
|
-
remoteChanged,
|
|
537
|
-
directMessage: "Both local and remote changed. Run /sync diff and resolve with push --force or pull --force.",
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
529
|
if (remoteChanged) {
|
|
541
530
|
await pull(ctx, options, factory);
|
|
542
531
|
return;
|
package/src/sync/sync-queries.ts
CHANGED
|
@@ -36,6 +36,10 @@ export async function status(
|
|
|
36
36
|
);
|
|
37
37
|
throwIfAborted(options.signal);
|
|
38
38
|
|
|
39
|
+
const continuous = config.continuous
|
|
40
|
+
? await (await import("./continuous-record.js")).continuousStatusLines(config)
|
|
41
|
+
: [];
|
|
42
|
+
throwIfAborted(options.signal);
|
|
39
43
|
const remoteText = head ? `remote: ${head.snapshotId} from ${head.machine} at ${head.createdAt}` : "remote: empty";
|
|
40
44
|
const warnings = syncSessionsWarnings(config);
|
|
41
45
|
setSyncStatus(ctx, undefined);
|
|
@@ -47,6 +51,7 @@ export async function status(
|
|
|
47
51
|
`publication safety: ${publicationCapabilityDescription(capability)}`,
|
|
48
52
|
`included content: ${config.include.join(", ") || "none"}`,
|
|
49
53
|
`sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
|
|
54
|
+
...continuous,
|
|
50
55
|
remoteText,
|
|
51
56
|
formatRemoteSelectionStatus(selectionState),
|
|
52
57
|
`local files: ${localFiles}`,
|
package/src/sync/text-merge.ts
CHANGED
|
@@ -30,11 +30,11 @@ export function text(bytes: Buffer) {
|
|
|
30
30
|
throw new Error("Unsupported text encoding.");
|
|
31
31
|
return decoded;
|
|
32
32
|
}
|
|
33
|
-
type Hunk = { start: number; end: number; lines: string[] };
|
|
34
|
-
function lines(value: string) {
|
|
33
|
+
export type Hunk = { start: number; end: number; lines: string[] };
|
|
34
|
+
export function lines(value: string) {
|
|
35
35
|
return value.match(/[^\n]*\n|[^\n]+$/gu) ?? [];
|
|
36
36
|
}
|
|
37
|
-
function edits(base: string[], next: string[], signal?: AbortSignal): Hunk[] {
|
|
37
|
+
export function edits(base: string[], next: string[], signal?: AbortSignal): Hunk[] {
|
|
38
38
|
const width = next.length + 1;
|
|
39
39
|
if ((base.length + 1) * width > 4_000_000) throw new Error("Text merge complexity bound exceeded.");
|
|
40
40
|
// Intern once so each charged DP cell compares fixed-size IDs, not long line strings.
|
package/src/sync-extension.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { completeSyncArguments, splitArgs } from "./commands/command.js";
|
|
3
3
|
import { handleCommand } from "./commands/command-handler.js";
|
|
4
4
|
import type { AnySyncConfig } from "./settings/settings-types.js";
|
|
@@ -19,6 +19,31 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
19
19
|
let sessionAbort = new AbortController();
|
|
20
20
|
let shutdownAbort: AbortController | undefined;
|
|
21
21
|
let initialization = Promise.resolve<AnySyncConfig | false | undefined>(undefined);
|
|
22
|
+
let continuous: ReturnType<typeof import("./sync/continuous-sync.js")["createContinuousSync"]> | undefined;
|
|
23
|
+
let owner: ExtensionContext["sessionManager"] | undefined;
|
|
24
|
+
let continuousPossible = false;
|
|
25
|
+
const foreground = new Map<ExtensionContext["sessionManager"], number>();
|
|
26
|
+
async function resumeContinuous(ctx: ExtensionContext, signal: AbortSignal, retry?: { setup?: string }) {
|
|
27
|
+
const isCurrent = () => !signal.aborted && !foreground.has(ctx.sessionManager) && owner === ctx.sessionManager;
|
|
28
|
+
if (!isCurrent() || !ctx.hasUI) return;
|
|
29
|
+
try {
|
|
30
|
+
const { loadConfigForCheck } = await import("./settings/config.js");
|
|
31
|
+
if (!isCurrent()) return;
|
|
32
|
+
const config = await loadConfigForCheck();
|
|
33
|
+
if (!isCurrent()) return;
|
|
34
|
+
continuousPossible = Boolean(config.continuous && config.agentMode);
|
|
35
|
+
if (!continuousPossible) {
|
|
36
|
+
await continuous?.stop(ctx.sessionManager);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { createContinuousSync } = await import("./sync/continuous-sync.js");
|
|
40
|
+
if (!isCurrent()) return;
|
|
41
|
+
continuous ??= createContinuousSync(loaders);
|
|
42
|
+
continuous.start(ctx, signal, Boolean(retry && (retry.setup === undefined || retry.setup === config.setupName)));
|
|
43
|
+
} catch {
|
|
44
|
+
/* Existing initialization/command paths report settings errors. */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
pi.registerCommand("sync", {
|
|
24
49
|
description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage",
|
|
@@ -28,23 +53,39 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
28
53
|
throw new Error("/sync requires TUI or RPC mode so results and safety prompts are observable.");
|
|
29
54
|
}
|
|
30
55
|
const signal = sessionAbort.signal;
|
|
31
|
-
const
|
|
32
|
-
if (signal.aborted) return;
|
|
33
|
-
await check.stop();
|
|
34
|
-
if (signal.aborted) return;
|
|
56
|
+
const manager = ctx.sessionManager;
|
|
35
57
|
const command = splitArgs(args)[0];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
let retry: { setup?: string } | undefined;
|
|
59
|
+
let mayResume = false;
|
|
60
|
+
foreground.set(manager, (foreground.get(manager) ?? 0) + 1);
|
|
61
|
+
try {
|
|
62
|
+
const ready = await initialization;
|
|
63
|
+
mayResume = ready !== false;
|
|
64
|
+
if (signal.aborted) return;
|
|
65
|
+
await continuous?.stop(manager);
|
|
66
|
+
if (signal.aborted) return;
|
|
67
|
+
await check.stop();
|
|
68
|
+
if (signal.aborted) return;
|
|
69
|
+
if (ready === false && command !== "help" && command !== "unlock" && command !== "migrate-state") {
|
|
70
|
+
ctx.ui.notify(
|
|
71
|
+
"pi-sync recovery required. Repair the startup error, then /reload before syncing. Use /sync help for recovery guidance.",
|
|
72
|
+
"error",
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const run = () =>
|
|
77
|
+
handleCommand(args, ctx, signal, loaders, attention, (setup) => {
|
|
78
|
+
retry = { setup };
|
|
79
|
+
});
|
|
80
|
+
// Drain background children before foreground guards. Help remains reachable during repair.
|
|
81
|
+
if (command === "migrate-state" || command === "help") await run();
|
|
82
|
+
else await withStateDirectoryAccess(run);
|
|
83
|
+
} finally {
|
|
84
|
+
const count = (foreground.get(manager) ?? 1) - 1;
|
|
85
|
+
if (count) foreground.set(manager, count);
|
|
86
|
+
else foreground.delete(manager);
|
|
87
|
+
if (mayResume) await resumeContinuous(ctx, signal, retry);
|
|
42
88
|
}
|
|
43
|
-
const run = () => handleCommand(args, ctx, signal, loaders, attention);
|
|
44
|
-
// Drain background cache/child cleanup before entering any foreground guard.
|
|
45
|
-
// Help must remain reachable when the state roots themselves need repair.
|
|
46
|
-
if (command === "migrate-state" || command === "help") await run();
|
|
47
|
-
else await withStateDirectoryAccess(run);
|
|
48
89
|
},
|
|
49
90
|
});
|
|
50
91
|
|
|
@@ -54,11 +95,15 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
54
95
|
sessionAbort.abort(new DOMException("Session replaced", "AbortError"));
|
|
55
96
|
sessionAbort = new AbortController();
|
|
56
97
|
const signal = sessionAbort.signal;
|
|
98
|
+
owner = ctx.sessionManager;
|
|
99
|
+
continuousPossible = false;
|
|
57
100
|
attention.reset(ctx);
|
|
58
101
|
const previous = initialization;
|
|
59
102
|
initialization = (async () => {
|
|
60
103
|
await previous;
|
|
61
104
|
if (signal.aborted) return false;
|
|
105
|
+
await continuous?.stop();
|
|
106
|
+
if (signal.aborted) return false;
|
|
62
107
|
await check.stop();
|
|
63
108
|
if (signal.aborted) return false;
|
|
64
109
|
try {
|
|
@@ -72,18 +117,27 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
72
117
|
}
|
|
73
118
|
})();
|
|
74
119
|
const ready = await initialization;
|
|
75
|
-
if (ready && !signal.aborted)
|
|
120
|
+
if (ready && !signal.aborted) {
|
|
121
|
+
if (ready.continuous && ready.agentMode) await resumeContinuous(ctx, signal);
|
|
122
|
+
else check.start(ctx, signal, ready);
|
|
123
|
+
}
|
|
76
124
|
});
|
|
77
125
|
|
|
78
126
|
pi.on("agent_start", async (_event, ctx) => {
|
|
127
|
+
await continuous?.stop(ctx.sessionManager);
|
|
79
128
|
await check.interrupt(ctx);
|
|
80
129
|
});
|
|
81
130
|
|
|
82
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
83
|
-
|
|
131
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
132
|
+
const signal = sessionAbort.signal;
|
|
133
|
+
if (!signal.aborted) {
|
|
134
|
+
check.settled(ctx);
|
|
135
|
+
if (continuousPossible) await resumeContinuous(ctx, signal);
|
|
136
|
+
}
|
|
84
137
|
});
|
|
85
138
|
|
|
86
139
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
140
|
+
if (continuous && owner && owner !== ctx.sessionManager) return;
|
|
87
141
|
sessionAbort.abort(new DOMException("Session shut down", "AbortError"));
|
|
88
142
|
attention.reset(ctx);
|
|
89
143
|
shutdownAbort?.abort(new DOMException("Session shut down again", "AbortError"));
|
|
@@ -94,6 +148,8 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
|
|
|
94
148
|
try {
|
|
95
149
|
const ready = await initialization;
|
|
96
150
|
if (signal.aborted) return;
|
|
151
|
+
await continuous?.stop(ctx.sessionManager);
|
|
152
|
+
if (signal.aborted) return;
|
|
97
153
|
await check.stop();
|
|
98
154
|
if (signal.aborted) return;
|
|
99
155
|
if (ready !== false && reason !== "reload") {
|