@narumitw/pi-sync 0.57.2 → 0.57.4
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 +5 -1
- package/dist/chunks/{agent-plan-K3CRYVXV.ts → agent-plan-3L7CRFDM.ts} +28 -11
- package/dist/chunks/agent-plan-3L7CRFDM.ts.map +7 -0
- package/dist/chunks/agent-refusal-retry-AH7TXOQT.ts +69 -0
- package/dist/chunks/agent-refusal-retry-AH7TXOQT.ts.map +7 -0
- package/dist/chunks/{agent-runner-E2JU2VZ6.ts → agent-runner-WMSCKSCB.ts} +7 -5
- package/dist/chunks/agent-runner-WMSCKSCB.ts.map +7 -0
- package/dist/chunks/{agent-store-A3F6IYWZ.ts → agent-store-55CZOCPX.ts} +25 -3
- package/dist/chunks/agent-store-55CZOCPX.ts.map +7 -0
- package/dist/chunks/{chunk-QGK445K7.ts → chunk-3ST7RSYN.ts} +1 -1
- package/dist/chunks/chunk-3ST7RSYN.ts.map +7 -0
- package/dist/chunks/{chunk-OWBAZA6J.ts → chunk-DT6TOCIG.ts} +2 -2
- package/dist/chunks/{chunk-JJTARUSO.ts → chunk-JCH3VXIO.ts} +3 -2
- package/dist/chunks/{chunk-JJTARUSO.ts.map → chunk-JCH3VXIO.ts.map} +2 -2
- package/dist/chunks/{chunk-OEWQVWLC.ts → chunk-ORGBI36T.ts} +4 -4
- package/dist/chunks/{chunk-EUMINX7L.ts → chunk-P7DD5PWZ.ts} +2 -2
- package/dist/chunks/{chunk-VJN7KU4J.ts → chunk-PG7NMFP2.ts} +83 -18
- package/dist/chunks/chunk-PG7NMFP2.ts.map +7 -0
- package/dist/chunks/{chunk-T3NC77TU.ts → chunk-R75IHAOJ.ts} +45 -18
- package/dist/chunks/chunk-R75IHAOJ.ts.map +7 -0
- package/dist/chunks/{chunk-4TQWFLOQ.ts → chunk-U3XRI2HF.ts} +28 -197
- package/dist/chunks/chunk-U3XRI2HF.ts.map +7 -0
- package/dist/chunks/chunk-WD6FJHCK.ts +140 -0
- package/dist/chunks/chunk-WD6FJHCK.ts.map +7 -0
- package/dist/chunks/{conflict-review-ZCGWFKEK.ts → conflict-review-T7GSOKNG.ts} +9 -8
- package/dist/chunks/{conflict-review-ZCGWFKEK.ts.map → conflict-review-T7GSOKNG.ts.map} +1 -1
- package/dist/chunks/{continuous-record-LFRVXQBD.ts → continuous-record-SQJ47CXN.ts} +3 -3
- package/dist/chunks/{continuous-sync-LAIMSW7D.ts → continuous-sync-U4HIKPQK.ts} +9 -9
- package/dist/chunks/{continuous-sync-LAIMSW7D.ts.map → continuous-sync-U4HIKPQK.ts.map} +2 -2
- package/dist/chunks/{git-backend-KYDZ673X.ts → git-backend-NYKSG5X6.ts} +4 -4
- package/dist/chunks/{manager-result-dispatcher-AGBBVDDS.ts → manager-result-dispatcher-HOCM2GDA.ts} +4 -4
- package/dist/chunks/{manager-ui-PSA2L6CE.ts → manager-ui-FXXO6TW4.ts} +54 -17
- package/dist/chunks/manager-ui-FXXO6TW4.ts.map +7 -0
- package/dist/chunks/{remote-selection-ui-5725JDNC.ts → remote-selection-ui-YFPGYGNE.ts} +3 -3
- package/dist/chunks/{s3-backend-Y7WSCLJI.ts → s3-backend-F7PR5FB3.ts} +4 -4
- package/dist/chunks/{setup-wizard-SOYOJQKP.ts → setup-wizard-OVHZYXNW.ts} +2 -2
- package/dist/chunks/{sync-inspection-XFJOAMJD.ts → sync-inspection-5MY2HI4P.ts} +3 -3
- package/dist/chunks/{sync-operations-W5HFFMTP.ts → sync-operations-GNPS6BLY.ts} +29 -17
- package/dist/chunks/sync-operations-GNPS6BLY.ts.map +7 -0
- package/dist/chunks/{webdav-backend-6K26F6LT.ts → webdav-backend-TGFUH3SF.ts} +4 -4
- package/dist/index.ts +28 -23
- package/dist/index.ts.map +2 -2
- package/docs/agent-continuous-hardening.md +79 -0
- package/docs/agent-mode-verification.md +2 -0
- package/docs/agent-quality-verification.md +41 -0
- package/docs/continuous-sync-verification.md +2 -0
- package/package.json +2 -2
- package/skills/configure-pi-sync/references/configuration.md +21 -5
- package/src/commands/command-execution.ts +7 -2
- package/src/commands/command-handler.ts +3 -1
- package/src/commands/command-types.ts +3 -1
- package/src/sync/agent-plan.ts +26 -9
- package/src/sync/agent-refusal-retry.ts +54 -0
- package/src/sync/agent-request.ts +3 -1
- package/src/sync/agent-runner.ts +8 -6
- package/src/sync/agent-store.ts +32 -1
- package/src/sync/agent-sync.ts +63 -13
- package/src/sync/continuous-lease.ts +2 -7
- package/src/sync/continuous-record.ts +95 -13
- package/src/sync/continuous-storage.ts +12 -3
- package/src/sync/continuous-sync.ts +2 -1
- package/src/sync/merged-sync.ts +37 -9
- package/src/sync/sync-mutations.ts +11 -0
- package/src/ui/cancellable-operation.ts +2 -0
- package/src/ui/manager-state.ts +1 -0
- package/src/ui/manager-ui.ts +59 -14
- package/src/ui/secret-input.ts +29 -211
- package/dist/chunks/agent-plan-K3CRYVXV.ts.map +0 -7
- package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +0 -7
- package/dist/chunks/agent-store-A3F6IYWZ.ts.map +0 -7
- package/dist/chunks/chunk-4TQWFLOQ.ts.map +0 -7
- package/dist/chunks/chunk-QGK445K7.ts.map +0 -7
- package/dist/chunks/chunk-QOC47PBC.ts +0 -97
- package/dist/chunks/chunk-QOC47PBC.ts.map +0 -7
- package/dist/chunks/chunk-T3NC77TU.ts.map +0 -7
- package/dist/chunks/chunk-VJN7KU4J.ts.map +0 -7
- package/dist/chunks/manager-ui-PSA2L6CE.ts.map +0 -7
- package/dist/chunks/sync-operations-W5HFFMTP.ts.map +0 -7
- /package/dist/chunks/{chunk-OWBAZA6J.ts.map → chunk-DT6TOCIG.ts.map} +0 -0
- /package/dist/chunks/{chunk-OEWQVWLC.ts.map → chunk-ORGBI36T.ts.map} +0 -0
- /package/dist/chunks/{chunk-EUMINX7L.ts.map → chunk-P7DD5PWZ.ts.map} +0 -0
- /package/dist/chunks/{continuous-record-LFRVXQBD.ts.map → continuous-record-SQJ47CXN.ts.map} +0 -0
- /package/dist/chunks/{git-backend-KYDZ673X.ts.map → git-backend-NYKSG5X6.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-AGBBVDDS.ts.map → manager-result-dispatcher-HOCM2GDA.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-5725JDNC.ts.map → remote-selection-ui-YFPGYGNE.ts.map} +0 -0
- /package/dist/chunks/{s3-backend-Y7WSCLJI.ts.map → s3-backend-F7PR5FB3.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-SOYOJQKP.ts.map → setup-wizard-OVHZYXNW.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-XFJOAMJD.ts.map → sync-inspection-5MY2HI4P.ts.map} +0 -0
- /package/dist/chunks/{webdav-backend-6K26F6LT.ts.map → webdav-backend-TGFUH3SF.ts.map} +0 -0
|
@@ -3,7 +3,13 @@ import { withLock } from "../state/lock.js";
|
|
|
3
3
|
import { withStateDirectoryAccess } from "../state/state-directory.js";
|
|
4
4
|
import { ContinuousError, type ContinuousErrorKind } from "./continuous-errors.js";
|
|
5
5
|
import { type ContinuousLease, continuousLeaseOwner } from "./continuous-lease.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CONTINUOUS_RECORD_MAX_BYTES,
|
|
8
|
+
continuousBinding,
|
|
9
|
+
readContinuousPrivate,
|
|
10
|
+
readContinuousPrivateDocument,
|
|
11
|
+
writeContinuousPrivate,
|
|
12
|
+
} from "./continuous-storage.js";
|
|
7
13
|
|
|
8
14
|
export const REQUEST_WINDOW_MS = 15 * 60_000;
|
|
9
15
|
export interface ContinuousRecord {
|
|
@@ -93,10 +99,12 @@ export function validateContinuousRecord(value: unknown, binding: string): asser
|
|
|
93
99
|
)
|
|
94
100
|
throw new Error("Invalid continuous sync state; evidence retained.");
|
|
95
101
|
}
|
|
102
|
+
function emptyContinuousRecord(binding: string): ContinuousRecord {
|
|
103
|
+
return { version: 1, binding, pending: true, phase: "waiting", next: 0, failures: 0, calls: [], attempts: {} };
|
|
104
|
+
}
|
|
96
105
|
export async function readContinuousRecord(binding: string): Promise<ContinuousRecord> {
|
|
97
106
|
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: {} };
|
|
107
|
+
if (value === undefined) return emptyContinuousRecord(binding);
|
|
100
108
|
validateContinuousRecord(value, binding);
|
|
101
109
|
return value;
|
|
102
110
|
}
|
|
@@ -131,16 +139,20 @@ export async function reserveContinuousRequest(
|
|
|
131
139
|
lease: ContinuousLease,
|
|
132
140
|
key: string,
|
|
133
141
|
clock: () => number = Date.now,
|
|
142
|
+
signal?: AbortSignal,
|
|
134
143
|
) {
|
|
144
|
+
signal?.throwIfAborted();
|
|
135
145
|
if (!digest(key)) throw new Error("Invalid continuous candidate key.");
|
|
136
146
|
let failure: ContinuousError | undefined;
|
|
137
147
|
await updateContinuousRecord(binding, lease, (record) => {
|
|
148
|
+
signal?.throwIfAborted();
|
|
138
149
|
const now = clock();
|
|
150
|
+
signal?.throwIfAborted();
|
|
139
151
|
normalizeContinuousClock(record, now);
|
|
140
152
|
if ((record.attempts[key] ?? 0) >= 2)
|
|
141
153
|
failure = new ContinuousError(
|
|
142
154
|
"model",
|
|
143
|
-
"Automatic request limit reached for this evidence.
|
|
155
|
+
"Automatic request limit reached for this evidence. Change the evidence or review /sync; Sync now does not reset this limit.",
|
|
144
156
|
);
|
|
145
157
|
else if (record.calls.length >= 2)
|
|
146
158
|
failure = new ContinuousError(
|
|
@@ -153,27 +165,97 @@ export async function reserveContinuousRequest(
|
|
|
153
165
|
record.calls.push({ key, at: now });
|
|
154
166
|
}
|
|
155
167
|
});
|
|
168
|
+
// Atomic publication is not interruptible. A reservation written before cancellation stays spent.
|
|
169
|
+
signal?.throwIfAborted();
|
|
156
170
|
if (failure) throw failure;
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
const recoveryActions: Record<ContinuousErrorKind, string> = {
|
|
174
|
+
transient: "Wait for the scheduled retry. Check network and service availability if failures continue.",
|
|
175
|
+
contention: "Wait for the active sync operation to finish. Do not remove a live lock.",
|
|
176
|
+
stale: "Wait for a fresh inspection after the retry delay. Local or remote evidence changed.",
|
|
177
|
+
authentication: "Repair storage or Pi model authentication, then reopen /sync. Do not share credentials.",
|
|
178
|
+
storage: "Repair private-state permissions, free space or storage access. Retain accounting and recovery evidence.",
|
|
179
|
+
unsafe:
|
|
180
|
+
"Remove secrets or repair unsupported conflicts. Changed evidence is inspected again; review /sync if needed.",
|
|
181
|
+
model:
|
|
182
|
+
"Review conflict evidence and Pi model access. Sync now reuses cached refusals; a refusal needs changed evidence or a separately confirmed retry.",
|
|
183
|
+
budget:
|
|
184
|
+
"Wait for the window allowance shown above. Waiting does not restore exhausted candidates or accounting capacity.",
|
|
185
|
+
unknown:
|
|
186
|
+
"Review /sync diagnostics and reconcile pending recovery before retrying. Do not delete private evidence or blindly republish.",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
function serializedBytes(record: ContinuousRecord): number {
|
|
190
|
+
// Keep this aligned with writeJson's private-document encoding.
|
|
191
|
+
return Buffer.byteLength(`${JSON.stringify(record, null, "\t")}\n`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Check space for one reservation without retaining or publishing the synthetic candidate. */
|
|
195
|
+
function hasReservationCapacity(record: ContinuousRecord, now: number): boolean {
|
|
196
|
+
const existing = Object.keys(record.attempts).find((key) => record.attempts[key] === 1);
|
|
197
|
+
let key = existing ?? "0".repeat(64);
|
|
198
|
+
if (!existing) {
|
|
199
|
+
for (let index = 1; record.attempts[key] !== undefined; index++) key = index.toString(16).padStart(64, "0");
|
|
200
|
+
}
|
|
201
|
+
return (
|
|
202
|
+
serializedBytes({
|
|
203
|
+
...record,
|
|
204
|
+
calls: [...record.calls, { key, at: now }],
|
|
205
|
+
attempts: { ...record.attempts, [key]: (record.attempts[key] ?? 0) + 1 },
|
|
206
|
+
}) <= CONTINUOUS_RECORD_MAX_BYTES
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
159
210
|
/** Read-only status: no directory creation, lease, mutation, request reservation or provider call. */
|
|
160
|
-
export async function continuousStatusLines(config: AnySyncConfig): Promise<string[]> {
|
|
211
|
+
export async function continuousStatusLines(config: AnySyncConfig, clock: () => number = Date.now): Promise<string[]> {
|
|
161
212
|
if (!config.continuous) return ["Continuous sync: Off"];
|
|
162
213
|
if (!config.agentMode) return ["Continuous sync: paused; enable Agent mode first"];
|
|
163
214
|
try {
|
|
164
|
-
const
|
|
215
|
+
const binding = await continuousBinding(config);
|
|
216
|
+
const document = await readContinuousPrivateDocument(`${binding}.json`);
|
|
217
|
+
const record = document === undefined ? emptyContinuousRecord(binding) : document.value;
|
|
218
|
+
validateContinuousRecord(record, binding);
|
|
219
|
+
const bytes = serializedBytes(record);
|
|
220
|
+
// Only this detached read is normalized. A Status read never publishes clock repairs or expires durable calls.
|
|
221
|
+
const now = clock();
|
|
222
|
+
normalizeContinuousClock(record, now);
|
|
223
|
+
const spent = Object.values(record.attempts).filter((count) => count === 2).length;
|
|
224
|
+
const eligible =
|
|
225
|
+
record.calls.length < 2 ? now : Math.min(...record.calls.map((call) => call.at)) + REQUEST_WINDOW_MS;
|
|
226
|
+
const capacityRecord = { ...record, calls: [...record.calls] };
|
|
227
|
+
normalizeContinuousClock(capacityRecord, eligible);
|
|
228
|
+
const available = hasReservationCapacity(capacityRecord, eligible);
|
|
229
|
+
const owner = await continuousLeaseOwner(binding).catch(
|
|
230
|
+
() => "Local scheduler lease: private owner metadata unavailable; retain evidence and repair access",
|
|
231
|
+
);
|
|
165
232
|
return [
|
|
166
|
-
`Continuous sync: ${record.phase}${record.error ? ` (${record.error})` : ""}`,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`
|
|
233
|
+
`Continuous sync: ${record.phase}${record.error ? ` (${record.error})` : ""}${document === undefined ? "; no accounting record yet" : ""}`,
|
|
234
|
+
owner,
|
|
235
|
+
"Scheduler owner model: unavailable; the observing session's selected model is not proof of the owner's model.",
|
|
236
|
+
`Automatic requests (rolling 15 minutes, window only): ${record.calls.length}/2 used; ${2 - record.calls.length} remaining.`,
|
|
237
|
+
`Next request eligibility: ${available ? (eligible === now ? "now (window allowance)" : `${new Date(eligible).toISOString()} (window allowance)`) : "unavailable (accounting full)"}. Candidate, capacity, ownership and safety checks still apply.`,
|
|
238
|
+
`Spent candidates: ${spent} exhausted / ${Object.keys(record.attempts).length} tracked; 2 automatic requests per candidate. Expired windows do not clear this limit.`,
|
|
239
|
+
`Accounting: ${bytes}/${CONTINUOUS_RECORD_MAX_BYTES} serialized bytes; ${Math.max(0, CONTINUOUS_RECORD_MAX_BYTES - bytes)} bytes free; ${document?.bytes ?? 0} bytes on disk${available ? "." : "; full for another reservation."}`,
|
|
240
|
+
...(!available
|
|
241
|
+
? [
|
|
242
|
+
"Accounting recovery: pause Continuous sync and seek reviewed recovery. Do not delete accounting, prune candidates or reset limits to regain requests.",
|
|
243
|
+
]
|
|
244
|
+
: []),
|
|
245
|
+
`Last successful background check: ${record.checked !== undefined ? new Date(record.checked).toISOString() : "not yet checked"}`,
|
|
246
|
+
`Last successful background transfer: ${record.transferred !== undefined ? new Date(record.transferred).toISOString() : "none"}`,
|
|
170
247
|
...(record.phase === "retry" ? [`Next background attempt: ${new Date(record.next).toISOString()}`] : []),
|
|
171
|
-
...(record.
|
|
172
|
-
|
|
248
|
+
...(record.error ? [`Recovery (${record.error}): ${recoveryActions[record.error]}`] : []),
|
|
249
|
+
...(record.phase === "blocked" && !record.error
|
|
250
|
+
? ["Review /sync diagnostics and repair changed evidence. Retain private recovery state."]
|
|
173
251
|
: []),
|
|
174
252
|
"Monitoring requires an eligible open Pi session. Status does not spend model requests or clear retry limits.",
|
|
175
253
|
];
|
|
176
254
|
} catch {
|
|
177
|
-
return [
|
|
255
|
+
return [
|
|
256
|
+
"Continuous sync: private state unavailable; usage, eligibility and accounting bytes cannot be verified.",
|
|
257
|
+
`Accounting capacity: ${CONTINUOUS_RECORD_MAX_BYTES} bytes; do not treat unavailable accounting as unused allowance.`,
|
|
258
|
+
"Recovery: pause Continuous sync, retain malformed, unknown or unsafe evidence, and repair private-state access. Do not delete accounting or reset limits.",
|
|
259
|
+
];
|
|
178
260
|
}
|
|
179
261
|
}
|
|
@@ -8,6 +8,8 @@ import { writeJson } from "../state/json-file.js";
|
|
|
8
8
|
import { stateDir } from "../state/state-directory.js";
|
|
9
9
|
import { ensureStateDir } from "../state/sync-state-store.js";
|
|
10
10
|
|
|
11
|
+
export const CONTINUOUS_RECORD_MAX_BYTES = 131072;
|
|
12
|
+
|
|
11
13
|
export function hashContinuous(value: unknown) {
|
|
12
14
|
return createHash("sha256")
|
|
13
15
|
.update(JSON.stringify(value) ?? "null")
|
|
@@ -30,6 +32,12 @@ export async function checkContinuousRoot() {
|
|
|
30
32
|
throw new Error("Unsafe continuous sync directory; evidence retained.");
|
|
31
33
|
}
|
|
32
34
|
export async function readContinuousPrivate(name: string): Promise<unknown> {
|
|
35
|
+
return (await readContinuousPrivateDocument(name))?.value;
|
|
36
|
+
}
|
|
37
|
+
/** Read-only metadata; uses the same private-file checks as runtime reads. */
|
|
38
|
+
export async function readContinuousPrivateDocument(
|
|
39
|
+
name: string,
|
|
40
|
+
): Promise<{ value: unknown; bytes: number } | undefined> {
|
|
33
41
|
try {
|
|
34
42
|
await checkContinuousRoot();
|
|
35
43
|
const file = path.join(continuousRoot(), name);
|
|
@@ -38,11 +46,12 @@ export async function readContinuousPrivate(name: string): Promise<unknown> {
|
|
|
38
46
|
!stat.isFile() ||
|
|
39
47
|
stat.isSymbolicLink() ||
|
|
40
48
|
stat.nlink !== 1 ||
|
|
41
|
-
stat.size >
|
|
49
|
+
stat.size > CONTINUOUS_RECORD_MAX_BYTES ||
|
|
42
50
|
(process.platform !== "win32" && stat.mode & 0o077)
|
|
43
51
|
)
|
|
44
52
|
throw new Error("Unsafe continuous sync record; evidence retained.");
|
|
45
|
-
|
|
53
|
+
const bytes = await fs.readFile(file);
|
|
54
|
+
return { value: JSON.parse(bytes.toString("utf8")), bytes: bytes.byteLength };
|
|
46
55
|
} catch (error) {
|
|
47
56
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
48
57
|
throw error;
|
|
@@ -51,5 +60,5 @@ export async function readContinuousPrivate(name: string): Promise<unknown> {
|
|
|
51
60
|
export async function writeContinuousPrivate(name: string, value: unknown) {
|
|
52
61
|
await checkContinuousRoot();
|
|
53
62
|
await readContinuousPrivate(name); // Never replace unsafe or malformed evidence.
|
|
54
|
-
await writeJson(path.join(continuousRoot(), name), value, { maxBytes:
|
|
63
|
+
await writeJson(path.join(continuousRoot(), name), value, { maxBytes: CONTINUOUS_RECORD_MAX_BYTES });
|
|
55
64
|
}
|
|
@@ -451,7 +451,8 @@ export function createContinuousSync(loaders: SyncLoaders, clock: ContinuousCloc
|
|
|
451
451
|
signal,
|
|
452
452
|
background: {
|
|
453
453
|
validate,
|
|
454
|
-
reserve: (key
|
|
454
|
+
reserve: (key, requestSignal) =>
|
|
455
|
+
reserveContinuousRequest(binding, lease, key, () => clock.now(), requestSignal),
|
|
455
456
|
},
|
|
456
457
|
},
|
|
457
458
|
(options) => operations.syncBoth(ctx, options),
|
package/src/sync/merged-sync.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
import { type ConflictResolution, resolveReviewedGroup } from "./conflict-resolution.js";
|
|
49
49
|
import { pruneCompletedConflicts } from "./conflict-retention.js";
|
|
50
50
|
import { resolveContentConflicts } from "./content-conflicts.js";
|
|
51
|
+
import { ContinuousError } from "./continuous-errors.js";
|
|
51
52
|
import { planFileMerge } from "./file-merge-planner.js";
|
|
52
53
|
import { overlayLocalFields, portableSnapshot, sameLocalFields } from "./local-fields.js";
|
|
53
54
|
import { applyMergedSnapshot, preflightMergedTargets } from "./merge-apply.js";
|
|
@@ -96,8 +97,16 @@ export async function mergeSync(
|
|
|
96
97
|
return false;
|
|
97
98
|
}
|
|
98
99
|
};
|
|
100
|
+
const validateRetryAuthorization = (config: AnySyncConfig) => {
|
|
101
|
+
if (
|
|
102
|
+
options.agentRetryRefusal &&
|
|
103
|
+
(!config.agentMode || options.auto || options.background || !ctx.hasUI || !["tui", "rpc"].includes(ctx.mode))
|
|
104
|
+
)
|
|
105
|
+
throw new Error("Refused-candidate retry requires foreground Agent mode in TUI or RPC.");
|
|
106
|
+
};
|
|
99
107
|
const config = await loadConfig(options.setup);
|
|
100
108
|
validateOwner(config);
|
|
109
|
+
validateRetryAuthorization(config);
|
|
101
110
|
const configToken = syncCheckConfigFingerprint(config);
|
|
102
111
|
const modelToken = JSON.stringify(ctx.model);
|
|
103
112
|
const sessionRoot = config.include.includes("sessions") ? await effectiveSessionRoot(ctx) : undefined;
|
|
@@ -112,6 +121,7 @@ export async function mergeSync(
|
|
|
112
121
|
validateOwner();
|
|
113
122
|
const latest = await loadConfig(options.setup);
|
|
114
123
|
validateOwner(latest);
|
|
124
|
+
validateRetryAuthorization(latest);
|
|
115
125
|
if (configToken !== syncCheckConfigFingerprint(latest))
|
|
116
126
|
throw new Error("Sync setup or settings changed during transfer; reopen sync.");
|
|
117
127
|
if (sessionRoot !== undefined) {
|
|
@@ -130,6 +140,8 @@ export async function mergeSync(
|
|
|
130
140
|
await validate();
|
|
131
141
|
const pending = await timings.measure("transfer journal read", () => readMergeJournal(config));
|
|
132
142
|
await validate();
|
|
143
|
+
if (pending && options.agentRetryRefusal)
|
|
144
|
+
throw new Error("A pending merge journal requires ordinary Sync now before retrying a refused candidate.");
|
|
133
145
|
if (pending) {
|
|
134
146
|
const recovered = await timings.measure("transfer journal recovery", () =>
|
|
135
147
|
completeJournal(
|
|
@@ -339,7 +351,8 @@ export async function mergeSync(
|
|
|
339
351
|
);
|
|
340
352
|
await validate();
|
|
341
353
|
if (plan.kind !== "planned" || (plan.conflicts.length && !config.partialSync))
|
|
342
|
-
throw new
|
|
354
|
+
throw new ContinuousError(
|
|
355
|
+
"unsafe",
|
|
343
356
|
"Agent mode paused: unsupported or refused conflict groups remain; no transfer was performed.",
|
|
344
357
|
);
|
|
345
358
|
}
|
|
@@ -456,7 +469,10 @@ export async function mergeSync(
|
|
|
456
469
|
const beforeEvidence = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
457
470
|
await validate();
|
|
458
471
|
if (!beforeEvidence || !backend.sameRevision(beforeEvidence.revision, head.revision))
|
|
459
|
-
throw new
|
|
472
|
+
throw new ContinuousError(
|
|
473
|
+
"stale",
|
|
474
|
+
"Remote changed before baseline acceptance; retry from a fresh observation.",
|
|
475
|
+
);
|
|
460
476
|
progress = await persistProgress();
|
|
461
477
|
await validate();
|
|
462
478
|
const currentLocal = await timings.measure("transfer local snapshot", () =>
|
|
@@ -468,14 +484,20 @@ export async function mergeSync(
|
|
|
468
484
|
syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !==
|
|
469
485
|
syncStateFingerprint(state)
|
|
470
486
|
)
|
|
471
|
-
throw new
|
|
487
|
+
throw new ContinuousError(
|
|
488
|
+
"stale",
|
|
472
489
|
"Local content or baseline changed before baseline acceptance; retry from a fresh observation.",
|
|
473
490
|
);
|
|
474
491
|
await validate();
|
|
475
492
|
const currentHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
476
493
|
await validate();
|
|
477
494
|
if (!currentHead || !backend.sameRevision(currentHead.revision, head.revision))
|
|
478
|
-
throw new
|
|
495
|
+
throw new ContinuousError(
|
|
496
|
+
"stale",
|
|
497
|
+
"Remote changed before baseline acceptance; retry from a fresh observation.",
|
|
498
|
+
);
|
|
499
|
+
await options.agentAcceptance?.commitCandidate?.();
|
|
500
|
+
await validate();
|
|
479
501
|
await timings.measure("transfer baseline write", () =>
|
|
480
502
|
writeAcceptedState(config, head, accepted, validate, progress),
|
|
481
503
|
);
|
|
@@ -512,7 +534,10 @@ export async function mergeSync(
|
|
|
512
534
|
);
|
|
513
535
|
await validate();
|
|
514
536
|
if (!config.skipSecretScan && scanSnapshot(upload).length)
|
|
515
|
-
throw new
|
|
537
|
+
throw new ContinuousError(
|
|
538
|
+
"unsafe",
|
|
539
|
+
"Refusing to merge possible secrets. Review managed content before syncing.",
|
|
540
|
+
);
|
|
516
541
|
if (!options.yes && !config.agentMode) {
|
|
517
542
|
const summary = mergedSyncSummary({
|
|
518
543
|
setupName: config.setupName,
|
|
@@ -545,17 +570,19 @@ export async function mergeSync(
|
|
|
545
570
|
createSnapshot(config.snapshotIdentity, snapshotOptions),
|
|
546
571
|
);
|
|
547
572
|
if (!sameHashes(fileHashMap(refreshed), fileHashMap(localRaw)))
|
|
548
|
-
throw new
|
|
573
|
+
throw new ContinuousError("stale", "Local content changed during review; no merged transfer was performed.");
|
|
549
574
|
if (
|
|
550
575
|
syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !==
|
|
551
576
|
syncStateFingerprint(state)
|
|
552
577
|
)
|
|
553
|
-
throw new
|
|
578
|
+
throw new ContinuousError("stale", "Sync baseline changed during review; retry from a fresh observation.");
|
|
554
579
|
await validate();
|
|
555
580
|
const refreshedHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
556
581
|
await validate();
|
|
557
582
|
if (!refreshedHead || !backend.sameRevision(refreshedHead.revision, head.revision))
|
|
558
|
-
throw new
|
|
583
|
+
throw new ContinuousError("stale", "Remote changed during review; no merged transfer was performed.");
|
|
584
|
+
await options.agentAcceptance?.commitCandidate?.();
|
|
585
|
+
await validate();
|
|
559
586
|
progress = await persistProgress();
|
|
560
587
|
await validate();
|
|
561
588
|
const backup = await timings.measure("transfer backup", () =>
|
|
@@ -589,7 +616,8 @@ export async function mergeSync(
|
|
|
589
616
|
if (!sameHashes(fileHashMap(atCommit), fileHashMap(localRaw)) || atCommitState !== journal.stateIdentity) {
|
|
590
617
|
await validate();
|
|
591
618
|
await clearMergeJournal(config);
|
|
592
|
-
throw new
|
|
619
|
+
throw new ContinuousError(
|
|
620
|
+
"stale",
|
|
593
621
|
"Local content or baseline changed before publication; candidate retired without transfer. Review a fresh sync.",
|
|
594
622
|
);
|
|
595
623
|
}
|
|
@@ -431,6 +431,17 @@ export async function syncBoth(
|
|
|
431
431
|
const config = await loadConfig(options.setup);
|
|
432
432
|
throwIfAborted(options.signal);
|
|
433
433
|
options.background?.validate(config);
|
|
434
|
+
if (options.agentRetryRefusal) {
|
|
435
|
+
if (!config.agentMode || options.auto || options.background || !ctx.hasUI)
|
|
436
|
+
throw new Error("Enable Agent mode and use the foreground manager to retry a cached refusal.");
|
|
437
|
+
const state = await readStateForConfig(config);
|
|
438
|
+
validateMutation();
|
|
439
|
+
const pending = await readMergeJournal(config);
|
|
440
|
+
validateMutation();
|
|
441
|
+
if (!config.include.length || !state.lastAppliedSnapshot || pending)
|
|
442
|
+
throw new Error("Retry requires an accepted baseline and no pending journal. Use Sync now or reviewed recovery.");
|
|
443
|
+
return mergeSync(ctx, options, factory);
|
|
444
|
+
}
|
|
434
445
|
if (config.include.length > 0 && options.auto && startupTransferEnabled(config))
|
|
435
446
|
return mergeSync(ctx, options, factory);
|
|
436
447
|
if (!options.auto && config.include.length > 0 && (await readMergeJournal(config)))
|
|
@@ -17,6 +17,7 @@ export type RunRoute = (
|
|
|
17
17
|
onCommit?: () => void,
|
|
18
18
|
target?: string,
|
|
19
19
|
onDialog?: (active: boolean) => void,
|
|
20
|
+
retryRefusal?: boolean,
|
|
20
21
|
) => Promise<RunRouteResult | undefined>;
|
|
21
22
|
|
|
22
23
|
export type CancellableOperationResult = RunRouteResult | { kind: "closed" } | { kind: "cancelled" };
|
|
@@ -106,6 +107,7 @@ export async function runCancellableOperation(
|
|
|
106
107
|
);
|
|
107
108
|
return {
|
|
108
109
|
render(width: number) {
|
|
110
|
+
if (width <= 0) return [];
|
|
109
111
|
const safeWidth = Math.max(1, width);
|
|
110
112
|
const lines = loader.render(safeWidth);
|
|
111
113
|
const bottomBorder = lines.at(-1);
|
package/src/ui/manager-state.ts
CHANGED
|
@@ -130,6 +130,7 @@ export async function describeManagerState(
|
|
|
130
130
|
: "Unavailable";
|
|
131
131
|
const canSwitch = (await countValidSyncSetups(configuredTargets, signal)) > 1;
|
|
132
132
|
const mainActions: string[] = [...MAIN_MENU_ACTIONS];
|
|
133
|
+
if (config.agentMode && !noSyncedContent) mainActions.splice(1, 0, "Retry refused model result…");
|
|
133
134
|
if (canSwitch) mainActions.push("Switch sync setup");
|
|
134
135
|
if (syncState?.unresolved?.length) mainActions.unshift("Review unresolved conflicts");
|
|
135
136
|
const ordinaryTitle = [
|
package/src/ui/manager-ui.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
3
|
import { type ActionMenuItem, defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
4
|
import { loadConfig, syncCheckConfigFingerprint } from "../settings/config.js";
|
|
4
5
|
import { operationBlocksChanges } from "../state/operation-availability.js";
|
|
@@ -33,6 +34,7 @@ export async function showSyncManager(
|
|
|
33
34
|
type Screen = "main";
|
|
34
35
|
type Action =
|
|
35
36
|
| "review-attention"
|
|
37
|
+
| "retry-refusal"
|
|
36
38
|
| "sync"
|
|
37
39
|
| "switch"
|
|
38
40
|
| "settings"
|
|
@@ -128,6 +130,27 @@ export async function showSyncManager(
|
|
|
128
130
|
const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
|
|
129
131
|
return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
|
|
130
132
|
},
|
|
133
|
+
"retry-refusal": async ({ signal: actionSignal }) => {
|
|
134
|
+
const signal = sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
|
|
135
|
+
const validate = captureMutationOwner(ctx, signal);
|
|
136
|
+
validate();
|
|
137
|
+
const retryRoute: RunRoute = (route, operationSignal, onCommit, target, onDialog) =>
|
|
138
|
+
runRoute(route, operationSignal, onCommit, target, onDialog, true);
|
|
139
|
+
const result = await runCancellableOperation(ctx, "Checking cached refusal…", "sync", retryRoute, {
|
|
140
|
+
commitAware: true,
|
|
141
|
+
cancelAcrossDialogs: true,
|
|
142
|
+
signal,
|
|
143
|
+
cancelledMessage: "Model retry cancelled. Submitted requests and committed writes cannot be undone.",
|
|
144
|
+
});
|
|
145
|
+
if (signal.aborted) return { kind: "close" };
|
|
146
|
+
validate();
|
|
147
|
+
if (result.kind === "decision-required" || result.kind === "remote-selection-required")
|
|
148
|
+
ctx.ui.notify(
|
|
149
|
+
"Model retry needs an established source and matching synced content. Use Sync now to review changed selection or recovery before retrying.",
|
|
150
|
+
"warning",
|
|
151
|
+
);
|
|
152
|
+
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
153
|
+
},
|
|
131
154
|
switch: async () => {
|
|
132
155
|
const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
|
|
133
156
|
return result === "pull-attempted" || result === "closed" ? { kind: "close" } : { kind: "stay" };
|
|
@@ -231,7 +254,25 @@ export async function showSyncManager(
|
|
|
231
254
|
},
|
|
232
255
|
},
|
|
233
256
|
});
|
|
234
|
-
|
|
257
|
+
const menuContext: ExtensionCommandContext = {
|
|
258
|
+
...ctx,
|
|
259
|
+
ui: {
|
|
260
|
+
...ctx.ui,
|
|
261
|
+
custom: (factory, customOptions) =>
|
|
262
|
+
ctx.ui.custom((...args) => {
|
|
263
|
+
const bound = (component: Component & { dispose?(): void }) => {
|
|
264
|
+
const render = component.render.bind(component);
|
|
265
|
+
// Kit menus use a one-cell layout floor; suppress it at a zero-column display boundary.
|
|
266
|
+
component.render = (width) => (width <= 0 ? [] : render(width));
|
|
267
|
+
return component;
|
|
268
|
+
};
|
|
269
|
+
const component = factory(...args);
|
|
270
|
+
// Keep synchronous factories synchronous, including adapters that inspect their returned component.
|
|
271
|
+
return component instanceof Promise ? component.then(bound) : bound(component);
|
|
272
|
+
}, customOptions),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
await runMenu(menuContext, menu, {
|
|
235
276
|
getState: async () => {
|
|
236
277
|
const pendingAttention = options.getAttention?.();
|
|
237
278
|
const observation = options.getObservation?.();
|
|
@@ -261,6 +302,7 @@ function syncMainMenuItem(
|
|
|
261
302
|
): ActionMenuItem<
|
|
262
303
|
"main",
|
|
263
304
|
| "sync"
|
|
305
|
+
| "retry-refusal"
|
|
264
306
|
| "pull"
|
|
265
307
|
| "push"
|
|
266
308
|
| "diff"
|
|
@@ -279,6 +321,7 @@ function syncMainMenuItem(
|
|
|
279
321
|
> {
|
|
280
322
|
const actions = {
|
|
281
323
|
"Sync now": "sync",
|
|
324
|
+
"Retry refused model result…": "retry-refusal",
|
|
282
325
|
"Pull from remote…": "pull",
|
|
283
326
|
"Push to remote…": "push",
|
|
284
327
|
"View differences": "diff",
|
|
@@ -303,18 +346,20 @@ function syncMainMenuItem(
|
|
|
303
346
|
label,
|
|
304
347
|
action,
|
|
305
348
|
description:
|
|
306
|
-
action === "
|
|
307
|
-
? "
|
|
308
|
-
: action === "
|
|
309
|
-
? "Manage"
|
|
310
|
-
: action === "
|
|
311
|
-
? "
|
|
312
|
-
: action === "
|
|
313
|
-
? "Transfer:
|
|
314
|
-
: action === "
|
|
315
|
-
? "Transfer:
|
|
316
|
-
: action === "
|
|
317
|
-
? "
|
|
318
|
-
:
|
|
349
|
+
action === "retry-refusal"
|
|
350
|
+
? "Transfer: confirm one fresh model request for a current cached refusal; may incur costs."
|
|
351
|
+
: action === "doctor"
|
|
352
|
+
? "Manage: review backend checks before running."
|
|
353
|
+
: action === "settings" || action === "history" || action === "switch"
|
|
354
|
+
? "Manage"
|
|
355
|
+
: action === "sync"
|
|
356
|
+
? "Transfer: choose a safe direction or review conflicts."
|
|
357
|
+
: action === "pull"
|
|
358
|
+
? "Transfer: remote → local; review writes and removals."
|
|
359
|
+
: action === "push"
|
|
360
|
+
? "Transfer: local → remote active snapshot; review publication."
|
|
361
|
+
: action === "status" || action === "diff"
|
|
362
|
+
? "Inspect: read current local and remote state without transferring files."
|
|
363
|
+
: undefined,
|
|
319
364
|
};
|
|
320
365
|
}
|