@narumitw/pi-sync 0.54.3 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -4
- 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 +84 -1
- package/package.json +3 -2
- 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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
2
|
+
import { withLock } from "../state/lock.js";
|
|
3
|
+
import { withStateDirectoryAccess } from "../state/state-directory.js";
|
|
4
|
+
import { ContinuousError, type ContinuousErrorKind } from "./continuous-errors.js";
|
|
5
|
+
import { type ContinuousLease, continuousLeaseOwner } from "./continuous-lease.js";
|
|
6
|
+
import { continuousBinding, readContinuousPrivate, writeContinuousPrivate } from "./continuous-storage.js";
|
|
7
|
+
|
|
8
|
+
export const REQUEST_WINDOW_MS = 15 * 60_000;
|
|
9
|
+
export interface ContinuousRecord {
|
|
10
|
+
version: 1;
|
|
11
|
+
binding: string;
|
|
12
|
+
pending: boolean;
|
|
13
|
+
phase: "waiting" | "syncing" | "retry" | "blocked";
|
|
14
|
+
next: number;
|
|
15
|
+
failures: number;
|
|
16
|
+
evidence?: string;
|
|
17
|
+
recovery?: string;
|
|
18
|
+
notice?: string;
|
|
19
|
+
configuration?: string;
|
|
20
|
+
error?: ContinuousErrorKind;
|
|
21
|
+
checked?: number;
|
|
22
|
+
transferred?: number;
|
|
23
|
+
calls: { key: string; at: number }[];
|
|
24
|
+
attempts: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
const digest = (value: unknown) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
|
|
27
|
+
const timestamp = (value: unknown) =>
|
|
28
|
+
typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 8_640_000_000_000_000;
|
|
29
|
+
export function validateContinuousRecord(value: unknown, binding: string): asserts value is ContinuousRecord {
|
|
30
|
+
const record = value as ContinuousRecord;
|
|
31
|
+
if (
|
|
32
|
+
!record ||
|
|
33
|
+
typeof record !== "object" ||
|
|
34
|
+
record.version !== 1 ||
|
|
35
|
+
record.binding !== binding ||
|
|
36
|
+
Object.keys(record).some(
|
|
37
|
+
(key) =>
|
|
38
|
+
![
|
|
39
|
+
"version",
|
|
40
|
+
"binding",
|
|
41
|
+
"pending",
|
|
42
|
+
"phase",
|
|
43
|
+
"next",
|
|
44
|
+
"failures",
|
|
45
|
+
"evidence",
|
|
46
|
+
"recovery",
|
|
47
|
+
"notice",
|
|
48
|
+
"configuration",
|
|
49
|
+
"error",
|
|
50
|
+
"checked",
|
|
51
|
+
"transferred",
|
|
52
|
+
"calls",
|
|
53
|
+
"attempts",
|
|
54
|
+
].includes(key),
|
|
55
|
+
) ||
|
|
56
|
+
typeof record.pending !== "boolean" ||
|
|
57
|
+
!["waiting", "syncing", "retry", "blocked"].includes(record.phase) ||
|
|
58
|
+
!timestamp(record.next) ||
|
|
59
|
+
!timestamp(record.failures) ||
|
|
60
|
+
record.failures > 30 ||
|
|
61
|
+
(record.evidence !== undefined && !digest(record.evidence)) ||
|
|
62
|
+
(record.recovery !== undefined && !digest(record.recovery)) ||
|
|
63
|
+
(record.notice !== undefined && !digest(record.notice)) ||
|
|
64
|
+
(record.configuration !== undefined && !digest(record.configuration)) ||
|
|
65
|
+
(record.error !== undefined &&
|
|
66
|
+
![
|
|
67
|
+
"transient",
|
|
68
|
+
"contention",
|
|
69
|
+
"stale",
|
|
70
|
+
"authentication",
|
|
71
|
+
"storage",
|
|
72
|
+
"unsafe",
|
|
73
|
+
"model",
|
|
74
|
+
"budget",
|
|
75
|
+
"unknown",
|
|
76
|
+
].includes(record.error)) ||
|
|
77
|
+
(record.checked !== undefined && !timestamp(record.checked)) ||
|
|
78
|
+
(record.transferred !== undefined && !timestamp(record.transferred)) ||
|
|
79
|
+
!Array.isArray(record.calls) ||
|
|
80
|
+
record.calls.length > 2 ||
|
|
81
|
+
record.calls.some(
|
|
82
|
+
(call) => !call || Object.keys(call).sort().join(",") !== "at,key" || !digest(call.key) || !timestamp(call.at),
|
|
83
|
+
) ||
|
|
84
|
+
!record.attempts ||
|
|
85
|
+
typeof record.attempts !== "object" ||
|
|
86
|
+
Array.isArray(record.attempts) ||
|
|
87
|
+
Object.entries(record.attempts).some(
|
|
88
|
+
([key, count]) => !digest(key) || !Number.isInteger(count) || count < 1 || count > 2,
|
|
89
|
+
) ||
|
|
90
|
+
record.calls.some(
|
|
91
|
+
(call) => (record.attempts[call.key] ?? 0) < record.calls.filter((other) => other.key === call.key).length,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
throw new Error("Invalid continuous sync state; evidence retained.");
|
|
95
|
+
}
|
|
96
|
+
export async function readContinuousRecord(binding: string): Promise<ContinuousRecord> {
|
|
97
|
+
const value = await readContinuousPrivate(`${binding}.json`);
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return { version: 1, binding, pending: true, phase: "waiting", next: 0, failures: 0, calls: [], attempts: {} };
|
|
100
|
+
validateContinuousRecord(value, binding);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
export async function updateContinuousRecord<T>(
|
|
104
|
+
binding: string,
|
|
105
|
+
lease: ContinuousLease,
|
|
106
|
+
update: (record: ContinuousRecord) => T,
|
|
107
|
+
): Promise<T> {
|
|
108
|
+
return withStateDirectoryAccess(() =>
|
|
109
|
+
withLock("continuous-state", async () => {
|
|
110
|
+
lease.assert();
|
|
111
|
+
const record = await readContinuousRecord(binding);
|
|
112
|
+
lease.assert();
|
|
113
|
+
const result = update(record);
|
|
114
|
+
validateContinuousRecord(record, binding);
|
|
115
|
+
lease.assert();
|
|
116
|
+
await writeContinuousPrivate(`${binding}.json`, record);
|
|
117
|
+
lease.assert();
|
|
118
|
+
return result;
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
export function normalizeContinuousClock(record: ContinuousRecord, now: number) {
|
|
123
|
+
// A backward wall-clock jump cannot grant more calls: retain them for a full window.
|
|
124
|
+
record.calls = record.calls
|
|
125
|
+
.map((call) => ({ ...call, at: Math.min(call.at, now) }))
|
|
126
|
+
.filter((call) => now - call.at < REQUEST_WINDOW_MS);
|
|
127
|
+
record.next = Math.min(record.next, now + (record.error === "budget" ? REQUEST_WINDOW_MS : 300_000));
|
|
128
|
+
}
|
|
129
|
+
export async function reserveContinuousRequest(
|
|
130
|
+
binding: string,
|
|
131
|
+
lease: ContinuousLease,
|
|
132
|
+
key: string,
|
|
133
|
+
clock: () => number = Date.now,
|
|
134
|
+
) {
|
|
135
|
+
if (!digest(key)) throw new Error("Invalid continuous candidate key.");
|
|
136
|
+
let failure: ContinuousError | undefined;
|
|
137
|
+
await updateContinuousRecord(binding, lease, (record) => {
|
|
138
|
+
const now = clock();
|
|
139
|
+
normalizeContinuousClock(record, now);
|
|
140
|
+
if ((record.attempts[key] ?? 0) >= 2)
|
|
141
|
+
failure = new ContinuousError(
|
|
142
|
+
"model",
|
|
143
|
+
"Automatic request limit reached for this evidence. Use Sync now for an explicit retry.",
|
|
144
|
+
);
|
|
145
|
+
else if (record.calls.length >= 2)
|
|
146
|
+
failure = new ContinuousError(
|
|
147
|
+
"budget",
|
|
148
|
+
"Automatic request window is full.",
|
|
149
|
+
Math.max(0, (record.calls[0]?.at ?? now) + REQUEST_WINDOW_MS - now),
|
|
150
|
+
);
|
|
151
|
+
else {
|
|
152
|
+
record.attempts[key] = (record.attempts[key] ?? 0) + 1;
|
|
153
|
+
record.calls.push({ key, at: now });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
if (failure) throw failure;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Read-only status: no directory creation, lease, mutation, request reservation or provider call. */
|
|
160
|
+
export async function continuousStatusLines(config: AnySyncConfig): Promise<string[]> {
|
|
161
|
+
if (!config.continuous) return ["Continuous sync: Off"];
|
|
162
|
+
if (!config.agentMode) return ["Continuous sync: paused; enable Agent mode first"];
|
|
163
|
+
try {
|
|
164
|
+
const record = await readContinuousRecord(await continuousBinding(config));
|
|
165
|
+
return [
|
|
166
|
+
`Continuous sync: ${record.phase}${record.error ? ` (${record.error})` : ""}`,
|
|
167
|
+
await continuousLeaseOwner(record.binding),
|
|
168
|
+
`Last successful background check: ${record.checked ? new Date(record.checked).toISOString() : "not yet checked"}`,
|
|
169
|
+
`Last successful background transfer: ${record.transferred ? new Date(record.transferred).toISOString() : "none"}`,
|
|
170
|
+
...(record.phase === "retry" ? [`Next background attempt: ${new Date(record.next).toISOString()}`] : []),
|
|
171
|
+
...(record.phase === "blocked"
|
|
172
|
+
? ["Background sync needs changed evidence or repair. Review /sync, or use Sync now for an explicit retry."]
|
|
173
|
+
: []),
|
|
174
|
+
"Monitoring requires an eligible open Pi session. Status does not spend model requests or clear retry limits.",
|
|
175
|
+
];
|
|
176
|
+
} catch {
|
|
177
|
+
return ["Continuous sync: private state unavailable; retain evidence and repair access before retrying."];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { backendIdentityCoordinates } from "../backends/backend-identity.js";
|
|
6
|
+
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
7
|
+
import { writeJson } from "../state/json-file.js";
|
|
8
|
+
import { stateDir } from "../state/state-directory.js";
|
|
9
|
+
import { ensureStateDir } from "../state/sync-state-store.js";
|
|
10
|
+
|
|
11
|
+
export function hashContinuous(value: unknown) {
|
|
12
|
+
return createHash("sha256")
|
|
13
|
+
.update(JSON.stringify(value) ?? "null")
|
|
14
|
+
.digest("hex");
|
|
15
|
+
}
|
|
16
|
+
export async function continuousBinding(config: AnySyncConfig) {
|
|
17
|
+
return hashContinuous([await fs.realpath(getAgentDir()), config.setupName, backendIdentityCoordinates(config)]);
|
|
18
|
+
}
|
|
19
|
+
export function continuousRoot() {
|
|
20
|
+
return path.join(stateDir(), "continuous");
|
|
21
|
+
}
|
|
22
|
+
export async function prepareContinuousRoot() {
|
|
23
|
+
await ensureStateDir();
|
|
24
|
+
await fs.mkdir(continuousRoot(), { recursive: true, mode: 0o700 });
|
|
25
|
+
await checkContinuousRoot();
|
|
26
|
+
}
|
|
27
|
+
export async function checkContinuousRoot() {
|
|
28
|
+
const stat = await fs.lstat(continuousRoot());
|
|
29
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.platform !== "win32" && stat.mode & 0o077))
|
|
30
|
+
throw new Error("Unsafe continuous sync directory; evidence retained.");
|
|
31
|
+
}
|
|
32
|
+
export async function readContinuousPrivate(name: string): Promise<unknown> {
|
|
33
|
+
try {
|
|
34
|
+
await checkContinuousRoot();
|
|
35
|
+
const file = path.join(continuousRoot(), name);
|
|
36
|
+
const stat = await fs.lstat(file);
|
|
37
|
+
if (
|
|
38
|
+
!stat.isFile() ||
|
|
39
|
+
stat.isSymbolicLink() ||
|
|
40
|
+
stat.nlink !== 1 ||
|
|
41
|
+
stat.size > 131072 ||
|
|
42
|
+
(process.platform !== "win32" && stat.mode & 0o077)
|
|
43
|
+
)
|
|
44
|
+
throw new Error("Unsafe continuous sync record; evidence retained.");
|
|
45
|
+
return JSON.parse(await fs.readFile(file, "utf8"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function writeContinuousPrivate(name: string, value: unknown) {
|
|
52
|
+
await checkContinuousRoot();
|
|
53
|
+
await readContinuousPrivate(name); // Never replace unsafe or malformed evidence.
|
|
54
|
+
await writeJson(path.join(continuousRoot(), name), value, { maxBytes: 131072 });
|
|
55
|
+
}
|