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