@davideasden/pi-undo 0.2.22 → 0.2.25
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 +36 -3
- package/extensions/pi-undo.ts +75 -15
- package/package.json +7 -6
- package/src/controller.ts +377 -45
- package/src/git-runner.ts +356 -51
- package/src/journal.ts +5 -2
- package/src/model.ts +2 -0
- package/src/native-metadata.ts +47 -87
- package/src/native-restore.ts +49 -56
- package/src/operation-context.ts +266 -0
- package/src/pi-runtime.ts +119 -15
- package/src/quarantine.ts +3 -0
- package/src/recovery.ts +3 -3
- package/src/restore-engine.ts +102 -76
- package/src/root-discovery.ts +46 -5
- package/src/snapshot-store.ts +39 -16
- package/src/status-reporter.ts +41 -1
- package/src/workspace-lock.ts +4 -1
package/src/controller.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
3
|
|
|
4
4
|
import { canonicalJson, checksum } from "./encoding.ts";
|
|
5
|
+
import {
|
|
6
|
+
checkOperation, createOperationScope, currentOperationContext, runWithOperationContext, operationFailure, isUnconfirmedExit,
|
|
7
|
+
configuredTimeout, operationHasUnconfirmedExit, rethrowOperationFailure, withRecoveryBudget,
|
|
8
|
+
type OperationScope, type ProcessDiagnostic,
|
|
9
|
+
} from "./operation-context.ts";
|
|
5
10
|
import type {
|
|
6
11
|
CheckpointRecord,
|
|
7
12
|
CursorState,
|
|
@@ -67,6 +72,11 @@ export interface ControllerDependencies {
|
|
|
67
72
|
}>;
|
|
68
73
|
readonly journal: JournalPort;
|
|
69
74
|
readonly clock: () => number;
|
|
75
|
+
readonly operationTimeoutMs?: number;
|
|
76
|
+
readonly onProgress?: (phase: string) => void;
|
|
77
|
+
readonly onOperationStart?: (opId: string) => void;
|
|
78
|
+
readonly onProcess?: (diagnostic: ProcessDiagnostic) => void;
|
|
79
|
+
readonly onOperationEnd?: (opId: string, result: OperationResult) => Promise<void>;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export interface JournalPort {
|
|
@@ -116,6 +126,7 @@ export interface InputContext {
|
|
|
116
126
|
|
|
117
127
|
export interface SessionBeforeTreeEvent {
|
|
118
128
|
readonly targetLeafId: string | null;
|
|
129
|
+
readonly signal?: AbortSignal;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
export interface SessionBeforeTreeResult {
|
|
@@ -148,6 +159,8 @@ export interface UndoController {
|
|
|
148
159
|
beforeTree(event: SessionBeforeTreeEvent): Promise<SessionBeforeTreeResult | undefined>;
|
|
149
160
|
afterTree(event: SessionTreeEvent): Promise<void>;
|
|
150
161
|
cancelTree?(): Promise<void>;
|
|
162
|
+
cancelOperation?(): boolean;
|
|
163
|
+
dispose?(): Promise<void>;
|
|
151
164
|
recover(): Promise<void>;
|
|
152
165
|
history(): HistoryState;
|
|
153
166
|
/** 当前 recovery lock 的原因;未锁定时返回 undefined。 */
|
|
@@ -160,6 +173,12 @@ export interface UndoController {
|
|
|
160
173
|
captureFailureReason(): string | undefined;
|
|
161
174
|
}
|
|
162
175
|
|
|
176
|
+
interface RunCompletion {
|
|
177
|
+
started: boolean;
|
|
178
|
+
readonly promise: Promise<void>;
|
|
179
|
+
readonly resolve: () => void;
|
|
180
|
+
}
|
|
181
|
+
|
|
163
182
|
interface StagedRun {
|
|
164
183
|
readonly rawPrompt: string;
|
|
165
184
|
readonly before: SnapshotManifest;
|
|
@@ -214,6 +233,26 @@ export class UndoControllerImpl implements UndoController {
|
|
|
214
233
|
private warmUpManifest: SnapshotManifest | undefined;
|
|
215
234
|
private pendingInputCapture: { readonly token: symbol; readonly promise: Promise<void> } | undefined;
|
|
216
235
|
private deferAgentStartUntilMessageEnd = false;
|
|
236
|
+
private runCompletion: RunCompletion | undefined;
|
|
237
|
+
private settling: Promise<void> | undefined;
|
|
238
|
+
private inputCommit: Promise<void> | undefined;
|
|
239
|
+
private treePreparation: Promise<SessionBeforeTreeResult | undefined> | undefined;
|
|
240
|
+
private treeApplication: Promise<void> | undefined;
|
|
241
|
+
private treeScope: OperationScope | undefined;
|
|
242
|
+
private treeCancellationRequested = false;
|
|
243
|
+
private disposed = false;
|
|
244
|
+
private activeScope: OperationScope | undefined;
|
|
245
|
+
private activeOperation: Promise<OperationResult> | undefined;
|
|
246
|
+
private readonly backgroundTasks = new Set<Promise<unknown>>();
|
|
247
|
+
private readonly backgroundScopes = new Set<OperationScope>();
|
|
248
|
+
private unsafeExit = false;
|
|
249
|
+
private operationId: string | undefined;
|
|
250
|
+
private transaction: {
|
|
251
|
+
descriptor: OperationDescriptor;
|
|
252
|
+
rollback?: SnapshotManifest;
|
|
253
|
+
target?: SnapshotManifest;
|
|
254
|
+
cursorDurable: boolean;
|
|
255
|
+
} | undefined;
|
|
217
256
|
private recoveryInFlight: Promise<void> | undefined;
|
|
218
257
|
private recoveryCompleted = false;
|
|
219
258
|
|
|
@@ -227,6 +266,64 @@ export class UndoControllerImpl implements UndoController {
|
|
|
227
266
|
this.recoveryCompleted = initialState.recoveryCompleted ?? false;
|
|
228
267
|
}
|
|
229
268
|
|
|
269
|
+
cancelOperation(): boolean {
|
|
270
|
+
if (this.activeScope === undefined || this.transaction?.cursorDurable) return false;
|
|
271
|
+
this.activeScope.cancel();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async dispose(): Promise<void> {
|
|
276
|
+
this.disposed = true;
|
|
277
|
+
this.cancelOperation();
|
|
278
|
+
for (const scope of this.backgroundScopes) scope.cancel();
|
|
279
|
+
await this.activeOperation;
|
|
280
|
+
await this.cancelTree();
|
|
281
|
+
await Promise.allSettled([...this.backgroundTasks, this.inputCommit, this.settling]);
|
|
282
|
+
this.staged = undefined;
|
|
283
|
+
this.finishRun(this.runCompletion);
|
|
284
|
+
if (this.unsafeExit) throw new Error("process_exit_unconfirmed:旧任务未确认停止,不能重建 runtime");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private operationTimeout(): number {
|
|
288
|
+
return this.dependencies.operationTimeoutMs ?? configuredTimeout("PI_UNDO_OPERATION_TIMEOUT_MS", 300_000);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private recordUnsafeExit(error: unknown): boolean {
|
|
292
|
+
if (!isUnconfirmedExit(error) && !operationHasUnconfirmedExit()) return false;
|
|
293
|
+
this.unsafeExit = true;
|
|
294
|
+
this.lock("process_exit_unconfirmed");
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private runBackground<T>(body: () => Promise<T>): Promise<T> {
|
|
299
|
+
const scope = createOperationScope({ timeoutMs: this.operationTimeout() });
|
|
300
|
+
this.backgroundScopes.add(scope);
|
|
301
|
+
const task = runWithOperationContext(scope.context, async () => {
|
|
302
|
+
try {
|
|
303
|
+
return await body();
|
|
304
|
+
} finally {
|
|
305
|
+
if (operationHasUnconfirmedExit()) this.recordUnsafeExit({ code: "process_exit_unconfirmed" });
|
|
306
|
+
}
|
|
307
|
+
}).finally(() => {
|
|
308
|
+
scope.dispose();
|
|
309
|
+
this.backgroundScopes.delete(scope);
|
|
310
|
+
this.backgroundTasks.delete(task);
|
|
311
|
+
});
|
|
312
|
+
this.backgroundTasks.add(task);
|
|
313
|
+
return task;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async withRecoveryBudget<T>(body: () => Promise<T>): Promise<T> {
|
|
317
|
+
try {
|
|
318
|
+
return await withRecoveryBudget(body, {
|
|
319
|
+
timeoutMs: this.operationTimeout(), onProgress: this.dependencies.onProgress,
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
this.recordUnsafeExit(error);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
230
327
|
history(): HistoryState {
|
|
231
328
|
return { undoCount: this.undoStack.length, redoCount: this.redoStack.length, locked: this.locked };
|
|
232
329
|
}
|
|
@@ -256,14 +353,15 @@ export class UndoControllerImpl implements UndoController {
|
|
|
256
353
|
}
|
|
257
354
|
|
|
258
355
|
warmUp(): void {
|
|
259
|
-
if (this.locked || this.warmUpInFlight !== undefined) return;
|
|
260
|
-
this.warmUpInFlight = (async () => {
|
|
356
|
+
if (this.disposed || this.locked || this.warmUpInFlight !== undefined) return;
|
|
357
|
+
this.warmUpInFlight = this.runBackground(async () => {
|
|
261
358
|
try {
|
|
262
359
|
this.warmUpManifest = await this.captureWithWorkspaceLock();
|
|
263
|
-
} catch {
|
|
360
|
+
} catch (error) {
|
|
361
|
+
this.recordUnsafeExit(error);
|
|
264
362
|
// 预热是 best-effort:失败静默,正式 capture 会再次尝试并上报。
|
|
265
363
|
}
|
|
266
|
-
})
|
|
364
|
+
});
|
|
267
365
|
}
|
|
268
366
|
|
|
269
367
|
captureFailureReason(): string | undefined {
|
|
@@ -275,13 +373,15 @@ export class UndoControllerImpl implements UndoController {
|
|
|
275
373
|
}
|
|
276
374
|
|
|
277
375
|
async prepareInput(text: string, context: InputContext): Promise<InputEventResult> {
|
|
278
|
-
if (this.promptDeferralInFlight) return { action: "defer" };
|
|
376
|
+
if (this.disposed || this.unsafeExit || this.promptDeferralInFlight) return { action: "defer" };
|
|
279
377
|
if (this.locked) return { action: "continue" };
|
|
280
378
|
if (this.operationInFlight) return { action: "defer" };
|
|
281
379
|
if (context.streaming || text.length === 0) return { action: "continue" };
|
|
380
|
+
if (this.disposed || this.runCompletion?.started || this.settling !== undefined) return { action: "defer" };
|
|
381
|
+
const run = this.beginRun();
|
|
282
382
|
try {
|
|
283
|
-
const before = await this.captureInputBaseline();
|
|
284
|
-
this.stageInput(text, before);
|
|
383
|
+
const before = await this.runBackground(() => this.captureInputBaseline());
|
|
384
|
+
if (this.runCompletion === run && !this.disposed) this.stageInput(text, before);
|
|
285
385
|
return { action: "continue" };
|
|
286
386
|
} catch (error) {
|
|
287
387
|
// 无法证明输入前状态:放弃记录本次历史,但绝不吞掉用户输入。
|
|
@@ -291,16 +391,18 @@ export class UndoControllerImpl implements UndoController {
|
|
|
291
391
|
}
|
|
292
392
|
|
|
293
393
|
beginInput(text: string, context: InputContext): InputEventResult {
|
|
294
|
-
if (this.promptDeferralInFlight) return { action: "defer" };
|
|
394
|
+
if (this.disposed || this.unsafeExit || this.promptDeferralInFlight) return { action: "defer" };
|
|
295
395
|
if (this.locked) return { action: "continue" };
|
|
296
396
|
if (this.operationInFlight) return { action: "defer" };
|
|
297
397
|
if (context.streaming || text.length === 0) return { action: "continue" };
|
|
398
|
+
if (this.disposed || this.runCompletion?.started || this.settling !== undefined) return { action: "defer" };
|
|
399
|
+
this.beginRun();
|
|
298
400
|
this.staged = undefined;
|
|
299
401
|
this.lastCaptureFailed = false;
|
|
300
402
|
this.lastCaptureFailureMessage = undefined;
|
|
301
403
|
this.deferAgentStartUntilMessageEnd = true;
|
|
302
404
|
const token = Symbol("input-capture");
|
|
303
|
-
const promise = this.captureInputForToken(text, token);
|
|
405
|
+
const promise = this.runBackground(() => this.captureInputForToken(text, token));
|
|
304
406
|
this.pendingInputCapture = { token, promise };
|
|
305
407
|
void promise.then(() => {
|
|
306
408
|
if (this.pendingInputCapture?.token === token) this.pendingInputCapture = undefined;
|
|
@@ -308,8 +410,16 @@ export class UndoControllerImpl implements UndoController {
|
|
|
308
410
|
return { action: "continue" };
|
|
309
411
|
}
|
|
310
412
|
|
|
311
|
-
|
|
312
|
-
if (
|
|
413
|
+
commitInput(): Promise<void> {
|
|
414
|
+
if (this.inputCommit !== undefined) return this.inputCommit;
|
|
415
|
+
const commit = this.commitCapturedInput();
|
|
416
|
+
this.inputCommit = commit.finally(() => { this.inputCommit = undefined; });
|
|
417
|
+
return this.inputCommit;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async commitCapturedInput(): Promise<void> {
|
|
421
|
+
if (!this.deferAgentStartUntilMessageEnd || this.disposed) return;
|
|
422
|
+
if (this.runCompletion !== undefined) this.runCompletion.started = true;
|
|
313
423
|
const pending = this.pendingInputCapture;
|
|
314
424
|
if (pending !== undefined) await pending.promise;
|
|
315
425
|
this.pendingInputCapture = undefined;
|
|
@@ -318,26 +428,48 @@ export class UndoControllerImpl implements UndoController {
|
|
|
318
428
|
}
|
|
319
429
|
|
|
320
430
|
async beforeAgentStart(): Promise<void> {
|
|
431
|
+
if (this.runCompletion !== undefined) this.runCompletion.started = true;
|
|
321
432
|
if (this.deferAgentStartUntilMessageEnd) return;
|
|
322
433
|
await this.startAgentRun();
|
|
323
434
|
}
|
|
324
435
|
|
|
436
|
+
private beginRun(): RunCompletion {
|
|
437
|
+
this.finishRun(this.runCompletion);
|
|
438
|
+
let resolve!: () => void;
|
|
439
|
+
const promise = new Promise<void>((done) => { resolve = done; });
|
|
440
|
+
const run = { started: false, promise, resolve };
|
|
441
|
+
this.runCompletion = run;
|
|
442
|
+
return run;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private finishRun(run: RunCompletion | undefined): void {
|
|
446
|
+
if (this.runCompletion === run) this.runCompletion = undefined;
|
|
447
|
+
run?.resolve();
|
|
448
|
+
}
|
|
449
|
+
|
|
325
450
|
private async startAgentRun(): Promise<void> {
|
|
326
|
-
if (this.locked || this.staged === undefined)
|
|
451
|
+
if (this.locked || this.disposed || this.staged === undefined) {
|
|
452
|
+
this.finishRun(this.runCompletion);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const staged = this.staged;
|
|
456
|
+
if (staged.startEntryId !== undefined) return;
|
|
327
457
|
try {
|
|
328
|
-
|
|
458
|
+
staged.startEntryId = await this.dependencies.appendControl("pi-undo:start", {
|
|
329
459
|
schemaVersion: 1,
|
|
330
|
-
beforeManifestId:
|
|
331
|
-
sourceLogicalLeaf:
|
|
460
|
+
beforeManifestId: staged.before.manifestId,
|
|
461
|
+
sourceLogicalLeaf: staged.sourceLogicalLeaf,
|
|
332
462
|
});
|
|
333
463
|
if (this.staged.startEntryId === null) {
|
|
334
464
|
this.lock("start_entry_missing");
|
|
465
|
+
this.finishRun(this.runCompletion);
|
|
335
466
|
this.staged = undefined;
|
|
336
467
|
await this.dependencies.appendControl("pi-undo:barrier", { reason: "start_entry_missing" }).catch(() => {});
|
|
337
468
|
return;
|
|
338
469
|
}
|
|
339
470
|
} catch {
|
|
340
471
|
this.lock("start_entry_append_failed");
|
|
472
|
+
this.finishRun(this.runCompletion);
|
|
341
473
|
this.staged = undefined;
|
|
342
474
|
return;
|
|
343
475
|
}
|
|
@@ -377,13 +509,26 @@ export class UndoControllerImpl implements UndoController {
|
|
|
377
509
|
|
|
378
510
|
private recordCaptureFailure(error: unknown): void {
|
|
379
511
|
this.lastCaptureFailed = true;
|
|
512
|
+
this.recordUnsafeExit(error);
|
|
380
513
|
this.lastCaptureFailureMessage = truncateReason(error instanceof Error ? error.message : String(error));
|
|
381
514
|
}
|
|
382
515
|
|
|
383
|
-
|
|
516
|
+
agentSettled(): Promise<void> {
|
|
517
|
+
if (this.disposed) return Promise.resolve();
|
|
518
|
+
if (this.settling !== undefined) return this.settling;
|
|
519
|
+
const run = this.runCompletion;
|
|
520
|
+
const settled = this.runBackground(() => this.settleRun());
|
|
521
|
+
this.settling = settled.finally(() => {
|
|
522
|
+
this.settling = undefined;
|
|
523
|
+
this.finishRun(run);
|
|
524
|
+
});
|
|
525
|
+
return this.settling;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private async settleRun(): Promise<void> {
|
|
384
529
|
const staged = this.staged;
|
|
385
530
|
this.staged = undefined;
|
|
386
|
-
if (this.locked || staged === undefined) return;
|
|
531
|
+
if (this.disposed || this.locked || staged === undefined) return;
|
|
387
532
|
if (staged.startEntryId === undefined || staged.startEntryId === null) {
|
|
388
533
|
// Pi 没有提供已落盘的 start entry ID,不能把后续 assistant 输出归属到该 checkpoint。
|
|
389
534
|
this.lock("start_entry_missing");
|
|
@@ -417,7 +562,11 @@ export class UndoControllerImpl implements UndoController {
|
|
|
417
562
|
measure("settled.prepareUndo", () =>
|
|
418
563
|
this.dependencies.prepareDurableRestore!(after, staged.before, changedPaths)),
|
|
419
564
|
];
|
|
420
|
-
await Promise.allSettled(preparations);
|
|
565
|
+
const outcomes = await Promise.allSettled(preparations);
|
|
566
|
+
for (const outcome of outcomes) {
|
|
567
|
+
if (outcome.status === "rejected" && this.recordUnsafeExit(outcome.reason)) throw outcome.reason;
|
|
568
|
+
}
|
|
569
|
+
checkOperation();
|
|
421
570
|
}
|
|
422
571
|
const endLeafId = this.dependencies.getLogicalLeafId() ?? staged.startEntryId;
|
|
423
572
|
const checkpoint = this.createCheckpoint(staged, after, changedPaths, userEntryId, endLeafId);
|
|
@@ -429,7 +578,8 @@ export class UndoControllerImpl implements UndoController {
|
|
|
429
578
|
return;
|
|
430
579
|
}
|
|
431
580
|
this.undoStack.push(checkpoint);
|
|
432
|
-
} catch {
|
|
581
|
+
} catch (error) {
|
|
582
|
+
this.recordUnsafeExit(error);
|
|
433
583
|
this.historyPaused = true;
|
|
434
584
|
this.undoStack.length = 0;
|
|
435
585
|
this.redoStack.length = 0;
|
|
@@ -451,28 +601,54 @@ export class UndoControllerImpl implements UndoController {
|
|
|
451
601
|
return this.runOperation("redo");
|
|
452
602
|
}
|
|
453
603
|
|
|
454
|
-
|
|
604
|
+
beforeTree(event: SessionBeforeTreeEvent): Promise<SessionBeforeTreeResult | undefined> {
|
|
605
|
+
if (this.treeScope !== undefined) return Promise.resolve({ cancel: true });
|
|
606
|
+
this.treeCancellationRequested = event.signal?.aborted ?? false;
|
|
607
|
+
const scope = createOperationScope({ timeoutMs: this.operationTimeout(), signal: event.signal });
|
|
608
|
+
this.treeScope = scope;
|
|
609
|
+
const preparation = runWithOperationContext(scope.context, () => this.prepareTree(event));
|
|
610
|
+
this.treePreparation = preparation.finally(() => {
|
|
611
|
+
this.treePreparation = undefined;
|
|
612
|
+
if (this.pendingTree === undefined) this.finishTreeScope();
|
|
613
|
+
});
|
|
614
|
+
return this.treePreparation;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private finishTreeScope(): void {
|
|
618
|
+
this.treeScope?.dispose();
|
|
619
|
+
this.treeScope = undefined;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private async prepareTree(event: SessionBeforeTreeEvent): Promise<SessionBeforeTreeResult | undefined> {
|
|
455
623
|
// Pi 0.86+ 在树导航期间也会让 isIdle() 暂时返回 false;只有 pi-undo
|
|
456
624
|
// 已经记录了尚未 settle 的 run 时,才需要中止并取消导航。
|
|
457
|
-
if (this.staged !== undefined
|
|
625
|
+
if (this.staged !== undefined || this.pendingInputCapture !== undefined ||
|
|
626
|
+
this.runCompletion !== undefined || this.settling !== undefined || this.disposed) {
|
|
458
627
|
await this.dependencies.abortAgent();
|
|
459
628
|
return { cancel: true };
|
|
460
629
|
}
|
|
461
|
-
if (this.locked || this.historyPaused || this.operationInFlight) return { cancel: true };
|
|
630
|
+
if (this.locked || this.historyPaused || this.operationInFlight || this.treeCancellationRequested) return { cancel: true };
|
|
462
631
|
this.operationInFlight = true;
|
|
463
632
|
let lease: { release(): Promise<void> } | undefined;
|
|
464
633
|
try {
|
|
634
|
+
checkOperation();
|
|
465
635
|
lease = await this.dependencies.acquireWorkspaceLock();
|
|
466
636
|
const rollback = await this.dependencies.capture();
|
|
637
|
+
checkOperation();
|
|
467
638
|
const targetState = await this.dependencies.resolveTreeTarget(event.targetLeafId);
|
|
468
639
|
const target = await this.dependencies.loadManifest(targetState.targetManifestId);
|
|
469
640
|
const plan = await this.dependencies.planRestore(rollback, target);
|
|
470
641
|
const descriptor = this.createDescriptor("tree", rollback, target, plan, targetState.logicalLeafId);
|
|
471
642
|
await this.dependencies.journal.prepare(descriptor, plan);
|
|
472
643
|
this.pendingTree = { descriptor, rollback, target, plan, undoStack: targetState.undoStack, lease };
|
|
644
|
+
if (this.treeCancellationRequested || this.treeScope?.context.signal.aborted) {
|
|
645
|
+
await this.withRecoveryBudget(() => this.cancelPreparedTree());
|
|
646
|
+
return { cancel: true };
|
|
647
|
+
}
|
|
473
648
|
return undefined;
|
|
474
|
-
} catch {
|
|
475
|
-
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.recordUnsafeExit(error);
|
|
651
|
+
if (lease !== undefined && !this.unsafeExit) {
|
|
476
652
|
await lease.release().catch(() => { this.lock("workspace_lock_release_failed"); });
|
|
477
653
|
}
|
|
478
654
|
this.operationInFlight = false;
|
|
@@ -480,12 +656,23 @@ export class UndoControllerImpl implements UndoController {
|
|
|
480
656
|
}
|
|
481
657
|
}
|
|
482
658
|
|
|
483
|
-
|
|
659
|
+
afterTree(event: SessionTreeEvent): Promise<void> {
|
|
660
|
+
if (this.treeApplication !== undefined) return this.treeApplication;
|
|
661
|
+
const application = runWithOperationContext(this.treeScope?.context, () => this.applyPreparedTree(event));
|
|
662
|
+
this.treeApplication = application.finally(() => {
|
|
663
|
+
this.treeApplication = undefined;
|
|
664
|
+
this.finishTreeScope();
|
|
665
|
+
});
|
|
666
|
+
return this.treeApplication;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private async applyPreparedTree(event: SessionTreeEvent): Promise<void> {
|
|
484
670
|
const pending = this.pendingTree;
|
|
485
671
|
if (pending === undefined) return;
|
|
486
672
|
this.pendingTree = undefined;
|
|
487
673
|
try {
|
|
488
|
-
|
|
674
|
+
const navigationTarget = event.navigationTargetLeafId === undefined ? event.newLeafId : event.navigationTargetLeafId;
|
|
675
|
+
if (navigationTarget !== pending.descriptor.toLogicalLeaf) {
|
|
489
676
|
this.lock("session_navigation_diverged");
|
|
490
677
|
await this.dependencies.journal.setPhase(pending.descriptor.opId, "RECOVERY_REQUIRED");
|
|
491
678
|
return;
|
|
@@ -494,6 +681,7 @@ export class UndoControllerImpl implements UndoController {
|
|
|
494
681
|
{ phase: "SESSION_MOVED", observedLogicalLeaf: event.newLeafId },
|
|
495
682
|
{ phase: "APPLYING" },
|
|
496
683
|
]);
|
|
684
|
+
checkOperation();
|
|
497
685
|
const applied = await this.dependencies.applyRestore(
|
|
498
686
|
pending.plan,
|
|
499
687
|
pending.target,
|
|
@@ -504,6 +692,7 @@ export class UndoControllerImpl implements UndoController {
|
|
|
504
692
|
await this.dependencies.journal.setPhase(pending.descriptor.opId, "RECOVERY_REQUIRED");
|
|
505
693
|
return;
|
|
506
694
|
}
|
|
695
|
+
checkOperation();
|
|
507
696
|
await this.dependencies.journal.setPhase(pending.descriptor.opId, "FILES_VERIFIED");
|
|
508
697
|
const cursorResult = await this.dependencies.appendCursor(
|
|
509
698
|
this.createTreeCursor(pending.descriptor, event.newLeafId, pending.undoStack),
|
|
@@ -517,25 +706,45 @@ export class UndoControllerImpl implements UndoController {
|
|
|
517
706
|
await this.dependencies.journal.markCommitted(pending.descriptor.opId);
|
|
518
707
|
this.undoStack.splice(0, this.undoStack.length, ...pending.undoStack);
|
|
519
708
|
this.redoStack.length = 0;
|
|
520
|
-
} catch {
|
|
709
|
+
} catch (error) {
|
|
710
|
+
this.recordUnsafeExit(error);
|
|
521
711
|
this.lock("tree_recovery_failed");
|
|
522
712
|
} finally {
|
|
523
|
-
|
|
713
|
+
if (!this.unsafeExit && !operationHasUnconfirmedExit()) {
|
|
714
|
+
await pending.lease.release().catch(() => { this.lock("workspace_lock_release_failed"); });
|
|
715
|
+
}
|
|
524
716
|
this.operationInFlight = false;
|
|
525
717
|
}
|
|
526
718
|
}
|
|
527
719
|
|
|
528
720
|
async cancelTree(): Promise<void> {
|
|
721
|
+
this.treeCancellationRequested = true;
|
|
722
|
+
this.treeScope?.cancel();
|
|
723
|
+
await this.treePreparation;
|
|
724
|
+
await this.treeApplication;
|
|
725
|
+
if (this.pendingTree !== undefined) await this.withRecoveryBudget(() => this.cancelPreparedTree());
|
|
726
|
+
this.finishTreeScope();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private async cancelPreparedTree(): Promise<void> {
|
|
529
730
|
const pending = this.pendingTree;
|
|
530
731
|
if (pending === undefined) return;
|
|
531
732
|
this.pendingTree = undefined;
|
|
532
733
|
try {
|
|
734
|
+
if (this.dependencies.getLogicalLeafId() !== pending.descriptor.fromLogicalLeaf) {
|
|
735
|
+
this.lock("tree_cancel_session_moved");
|
|
736
|
+
await this.dependencies.journal.setPhase(pending.descriptor.opId, "RECOVERY_REQUIRED");
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
533
739
|
await this.dependencies.journal.setPhase(pending.descriptor.opId, "ABORTING");
|
|
534
740
|
await this.dependencies.journal.setPhase(pending.descriptor.opId, "ABORTED");
|
|
535
|
-
} catch {
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.recordUnsafeExit(error);
|
|
536
743
|
this.lock("tree_cancel_failed");
|
|
537
744
|
} finally {
|
|
538
|
-
|
|
745
|
+
if (!this.unsafeExit && !operationHasUnconfirmedExit()) {
|
|
746
|
+
await pending.lease.release().catch(() => { this.lock("workspace_lock_release_failed"); });
|
|
747
|
+
}
|
|
539
748
|
this.operationInFlight = false;
|
|
540
749
|
}
|
|
541
750
|
}
|
|
@@ -561,10 +770,33 @@ export class UndoControllerImpl implements UndoController {
|
|
|
561
770
|
await recovery;
|
|
562
771
|
}
|
|
563
772
|
|
|
564
|
-
private
|
|
773
|
+
private runOperation(action: "undo" | "redo"): Promise<OperationResult> {
|
|
774
|
+
if (this.disposed || this.operationInFlight || this.activeOperation !== undefined) return Promise.resolve({ code: "busy", changedFiles: 0 });
|
|
775
|
+
this.operationId = `op-${randomUUID()}`;
|
|
776
|
+
this.dependencies.onOperationStart?.(this.operationId);
|
|
777
|
+
const scope = createOperationScope({
|
|
778
|
+
timeoutMs: this.operationTimeout(), onProgress: this.dependencies.onProgress, onProcess: this.dependencies.onProcess,
|
|
779
|
+
});
|
|
780
|
+
this.activeScope = scope;
|
|
781
|
+
const opId = this.operationId;
|
|
782
|
+
const task = runWithOperationContext(scope.context, () => this.performOperation(action)).then(async (result) => {
|
|
783
|
+
await this.dependencies.onOperationEnd?.(opId, result).catch(() => {});
|
|
784
|
+
return result;
|
|
785
|
+
});
|
|
786
|
+
this.activeOperation = task.finally(() => {
|
|
787
|
+
scope.dispose();
|
|
788
|
+
this.activeScope = undefined;
|
|
789
|
+
this.activeOperation = undefined;
|
|
790
|
+
this.transaction = undefined;
|
|
791
|
+
this.operationId = undefined;
|
|
792
|
+
});
|
|
793
|
+
return this.activeOperation;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private async performOperation(action: "undo" | "redo"): Promise<OperationResult> {
|
|
565
797
|
if (this.locked) return this.recoveryResult();
|
|
566
798
|
if (this.operationInFlight) return { code: "busy", changedFiles: 0 };
|
|
567
|
-
const profile = new OperationProfiler();
|
|
799
|
+
const profile = new OperationProfiler(this.dependencies.onProgress);
|
|
568
800
|
const done = (result: OperationResult): OperationResult => profile.attach(result);
|
|
569
801
|
this.operationInFlight = true;
|
|
570
802
|
this.operationAction = action;
|
|
@@ -574,16 +806,25 @@ export class UndoControllerImpl implements UndoController {
|
|
|
574
806
|
let lease: { release(): Promise<void> } | undefined;
|
|
575
807
|
try {
|
|
576
808
|
if (!await profile.measure("idle", () => this.ensureIdle())) {
|
|
809
|
+
checkOperation();
|
|
577
810
|
return done({ code: "idle_timeout", changedFiles: 0 });
|
|
578
811
|
}
|
|
579
|
-
|
|
812
|
+
if (!await profile.measure("checkpoint", () => this.waitForCheckpoint())) {
|
|
813
|
+
checkOperation();
|
|
814
|
+
return done({ code: "idle_timeout", changedFiles: 0 });
|
|
815
|
+
}
|
|
816
|
+
if (this.locked) return done(this.recoveryResult());
|
|
817
|
+
if (this.historyPaused) return done({ code: "history_paused", changedFiles: 0 });
|
|
818
|
+
if (this.disposed) return done({ code: "busy", changedFiles: 0 });
|
|
819
|
+
// Pi 空闲早于扩展 settled 完成;只有本轮完成凭据已终结才能选择目标。
|
|
580
820
|
const redo = action === "redo" ? this.redoStack.at(-1) : undefined;
|
|
581
821
|
const checkpoint = action === "undo" ? this.undoStack.at(-1) : redo?.checkpoint;
|
|
582
822
|
if (checkpoint === undefined) return done(noop());
|
|
583
823
|
const targetManifestId = redo?.targetManifestId;
|
|
584
824
|
try {
|
|
585
825
|
lease = await profile.measure("lock", () => this.dependencies.acquireWorkspaceLock());
|
|
586
|
-
} catch {
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (operationFailure(error) !== undefined || isUnconfirmedExit(error)) throw error;
|
|
587
828
|
return done({ code: "busy", changedFiles: 0 });
|
|
588
829
|
}
|
|
589
830
|
if (checkpoint.changedPaths.length === 0) {
|
|
@@ -606,7 +847,8 @@ export class UndoControllerImpl implements UndoController {
|
|
|
606
847
|
restoreTargetManifestId,
|
|
607
848
|
checkpoint.changedPaths,
|
|
608
849
|
));
|
|
609
|
-
} catch {
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (operationFailure(error) !== undefined || isUnconfirmedExit(error)) throw error;
|
|
610
852
|
return done({ code: "capture_failed", changedFiles: 0 });
|
|
611
853
|
}
|
|
612
854
|
let target: SnapshotManifest;
|
|
@@ -618,10 +860,12 @@ export class UndoControllerImpl implements UndoController {
|
|
|
618
860
|
plan = await profile.measure("plan", () =>
|
|
619
861
|
this.dependencies.planRestore(rollback, target, checkpoint.changedPaths));
|
|
620
862
|
targetLogicalLeaf = this.dependencies.resolveSessionTarget(action, checkpoint);
|
|
621
|
-
} catch {
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (operationFailure(error) !== undefined || isUnconfirmedExit(error)) throw error;
|
|
622
865
|
return done({ code: "restore_failed_safe", changedFiles: 0 });
|
|
623
866
|
}
|
|
624
867
|
const descriptor = this.createDescriptor(action, rollback, target, plan, targetLogicalLeaf);
|
|
868
|
+
this.transaction = { descriptor, rollback, target, cursorDurable: false };
|
|
625
869
|
await profile.measure("journal", () => this.dependencies.journal.prepare(descriptor, plan));
|
|
626
870
|
const navigation = await profile.measure("navigate", () =>
|
|
627
871
|
this.dependencies.navigateSession(action, checkpoint));
|
|
@@ -645,8 +889,9 @@ export class UndoControllerImpl implements UndoController {
|
|
|
645
889
|
const applied = await profile.measure("apply", () =>
|
|
646
890
|
this.dependencies.applyRestore(plan, target, { opId: descriptor.opId }));
|
|
647
891
|
if (applied.code !== "ok") {
|
|
648
|
-
|
|
649
|
-
|
|
892
|
+
const result = await this.compensate(descriptor, rollback, target, applied);
|
|
893
|
+
return done(result.code === "restore_failed_safe" && applied.failureCode !== undefined
|
|
894
|
+
? { ...result, code: applied.failureCode } : result);
|
|
650
895
|
}
|
|
651
896
|
await profile.measure("journal", () =>
|
|
652
897
|
this.dependencies.journal.setPhase(descriptor.opId, "FILES_VERIFIED"));
|
|
@@ -665,18 +910,32 @@ export class UndoControllerImpl implements UndoController {
|
|
|
665
910
|
totalPaths: applied.totalPaths,
|
|
666
911
|
})));
|
|
667
912
|
}
|
|
913
|
+
this.transaction.cursorDurable = true;
|
|
668
914
|
await profile.measure("commit", async () => {
|
|
669
915
|
await this.dependencies.journal.setPhase(descriptor.opId, "CURSOR_COMMITTED");
|
|
670
916
|
await this.dependencies.journal.markCommitted(descriptor.opId);
|
|
671
917
|
});
|
|
672
918
|
this.lastSafetyManifestId = rollback.manifestId;
|
|
673
919
|
return done(this.advanceHistory(action, checkpoint, { code: "ok", changedFiles: applied.verifiedPaths }));
|
|
674
|
-
} catch {
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (this.recordUnsafeExit(error)) return done(this.recoveryResult());
|
|
922
|
+
const reason = operationFailure(error);
|
|
923
|
+
const tx = this.transaction;
|
|
924
|
+
if (reason !== undefined && !tx?.cursorDurable) {
|
|
925
|
+
if (tx !== undefined) {
|
|
926
|
+
const recovered = tx.rollback !== undefined && tx.target !== undefined
|
|
927
|
+
? await this.compensate(tx.descriptor, tx.rollback, tx.target, { code: "restore_failed_safe", verifiedPaths: 0, totalPaths: 0 })
|
|
928
|
+
: await this.compensateSessionOnly(tx.descriptor);
|
|
929
|
+
if (recovered.code === "recovery_required") return done(recovered);
|
|
930
|
+
}
|
|
931
|
+
return done({ code: reason, changedFiles: 0 });
|
|
932
|
+
}
|
|
675
933
|
this.lock("operation_failed");
|
|
676
934
|
return done(this.recoveryResult());
|
|
677
935
|
} finally {
|
|
936
|
+
if (operationHasUnconfirmedExit()) this.recordUnsafeExit({ code: "process_exit_unconfirmed" });
|
|
678
937
|
const activeLease = lease;
|
|
679
|
-
if (activeLease !== undefined) {
|
|
938
|
+
if (activeLease !== undefined && !this.unsafeExit) {
|
|
680
939
|
await profile.measure("unlock", () =>
|
|
681
940
|
activeLease.release().catch(() => { this.lock("workspace_lock_release_failed"); }));
|
|
682
941
|
}
|
|
@@ -725,6 +984,7 @@ export class UndoControllerImpl implements UndoController {
|
|
|
725
984
|
plan,
|
|
726
985
|
targetLogicalLeaf,
|
|
727
986
|
);
|
|
987
|
+
this.transaction = { descriptor, cursorDurable: false };
|
|
728
988
|
await profile.measure("journal", () => this.dependencies.journal.prepare(descriptor, plan));
|
|
729
989
|
const navigation = await profile.measure("navigate", () =>
|
|
730
990
|
this.dependencies.navigateSession(action, checkpoint));
|
|
@@ -757,6 +1017,7 @@ export class UndoControllerImpl implements UndoController {
|
|
|
757
1017
|
if (cursorResult.kind === "volatile") {
|
|
758
1018
|
return profile.measure("compensate", () => this.compensateSessionOnly(descriptor));
|
|
759
1019
|
}
|
|
1020
|
+
this.transaction.cursorDurable = true;
|
|
760
1021
|
await profile.measure("commit", async () => {
|
|
761
1022
|
await this.dependencies.journal.setPhase(descriptor.opId, "CURSOR_COMMITTED");
|
|
762
1023
|
await this.dependencies.journal.markCommitted(descriptor.opId);
|
|
@@ -766,6 +1027,15 @@ export class UndoControllerImpl implements UndoController {
|
|
|
766
1027
|
}
|
|
767
1028
|
|
|
768
1029
|
private async compensateSessionOnly(descriptor: OperationDescriptor): Promise<OperationResult> {
|
|
1030
|
+
try {
|
|
1031
|
+
return await this.withRecoveryBudget(() => this.compensateSessionOnlyWithinBudget(descriptor));
|
|
1032
|
+
} catch {
|
|
1033
|
+
this.lock("session_only_recovery_failed");
|
|
1034
|
+
return this.recoveryResult();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private async compensateSessionOnlyWithinBudget(descriptor: OperationDescriptor): Promise<OperationResult> {
|
|
769
1039
|
try {
|
|
770
1040
|
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTING");
|
|
771
1041
|
if (!await this.dependencies.restoreSessionLeaf(descriptor.fromLogicalLeaf)) {
|
|
@@ -800,12 +1070,27 @@ export class UndoControllerImpl implements UndoController {
|
|
|
800
1070
|
}
|
|
801
1071
|
}
|
|
802
1072
|
|
|
1073
|
+
private async waitForCheckpoint(): Promise<boolean> {
|
|
1074
|
+
const deadline = currentOperationContext()?.deadline ?? Date.now() + this.operationTimeout();
|
|
1075
|
+
for (const task of [this.pendingInputCapture?.promise, this.inputCommit]) {
|
|
1076
|
+
if (task !== undefined && !await waitForCompletion(task, deadline)) return false;
|
|
1077
|
+
}
|
|
1078
|
+
const run = this.runCompletion;
|
|
1079
|
+
if (run?.started) return waitForCompletion(run.promise, deadline);
|
|
1080
|
+
// 输入预检失败时没有 message_end/settled;候选快照不属于已启动的 run。
|
|
1081
|
+
this.staged = undefined;
|
|
1082
|
+
this.deferAgentStartUntilMessageEnd = false;
|
|
1083
|
+
this.finishRun(run);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
803
1087
|
private async ensureIdle(): Promise<boolean> {
|
|
804
1088
|
if (this.dependencies.isAgentIdle()) return true;
|
|
805
1089
|
try {
|
|
806
1090
|
await this.dependencies.abortAgent();
|
|
807
1091
|
return await this.dependencies.waitForIdle(this.dependencies.clock() + 30_000);
|
|
808
|
-
} catch {
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
rethrowOperationFailure(error);
|
|
809
1094
|
return false;
|
|
810
1095
|
}
|
|
811
1096
|
}
|
|
@@ -818,8 +1103,11 @@ export class UndoControllerImpl implements UndoController {
|
|
|
818
1103
|
const lease = await this.dependencies.acquireWorkspaceLock();
|
|
819
1104
|
try {
|
|
820
1105
|
return await this.dependencies.capture();
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
this.recordUnsafeExit(error);
|
|
1108
|
+
throw error;
|
|
821
1109
|
} finally {
|
|
822
|
-
await lease.release();
|
|
1110
|
+
if (!this.unsafeExit) await lease.release();
|
|
823
1111
|
}
|
|
824
1112
|
}
|
|
825
1113
|
|
|
@@ -829,8 +1117,11 @@ export class UndoControllerImpl implements UndoController {
|
|
|
829
1117
|
const lease = await this.dependencies.acquireWorkspaceLock();
|
|
830
1118
|
try {
|
|
831
1119
|
return await captureBaseline(baseline);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
this.recordUnsafeExit(error);
|
|
1122
|
+
throw error;
|
|
832
1123
|
} finally {
|
|
833
|
-
await lease.release();
|
|
1124
|
+
if (!this.unsafeExit) await lease.release();
|
|
834
1125
|
}
|
|
835
1126
|
}
|
|
836
1127
|
|
|
@@ -843,6 +1134,7 @@ export class UndoControllerImpl implements UndoController {
|
|
|
843
1134
|
try {
|
|
844
1135
|
return await this.captureBaselineWithWorkspaceLock(baseline);
|
|
845
1136
|
} catch (error) {
|
|
1137
|
+
rethrowOperationFailure(error);
|
|
846
1138
|
if (!isTransientCaptureFailure(error)) throw error;
|
|
847
1139
|
await sleep(250);
|
|
848
1140
|
return await this.captureBaselineWithWorkspaceLock(baseline);
|
|
@@ -854,6 +1146,21 @@ export class UndoControllerImpl implements UndoController {
|
|
|
854
1146
|
rollback: SnapshotManifest,
|
|
855
1147
|
target: SnapshotManifest,
|
|
856
1148
|
failure: RestoreResult,
|
|
1149
|
+
): Promise<OperationResult> {
|
|
1150
|
+
try {
|
|
1151
|
+
this.dependencies.onProgress?.("compensate");
|
|
1152
|
+
return await this.withRecoveryBudget(() => this.compensateWithinBudget(descriptor, rollback, target, failure));
|
|
1153
|
+
} catch {
|
|
1154
|
+
this.lock("compensation_failed");
|
|
1155
|
+
return this.recoveryResult();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private async compensateWithinBudget(
|
|
1160
|
+
descriptor: OperationDescriptor,
|
|
1161
|
+
rollback: SnapshotManifest,
|
|
1162
|
+
target: SnapshotManifest,
|
|
1163
|
+
failure: RestoreResult,
|
|
857
1164
|
): Promise<OperationResult> {
|
|
858
1165
|
try {
|
|
859
1166
|
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTING");
|
|
@@ -870,7 +1177,8 @@ export class UndoControllerImpl implements UndoController {
|
|
|
870
1177
|
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTED");
|
|
871
1178
|
return { code: "restore_failed_safe", changedFiles: failure.verifiedPaths };
|
|
872
1179
|
}
|
|
873
|
-
} catch {
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
this.recordUnsafeExit(error);
|
|
874
1182
|
// 下面统一进入 recovery lock。
|
|
875
1183
|
}
|
|
876
1184
|
this.lock("compensation_failed");
|
|
@@ -927,7 +1235,7 @@ export class UndoControllerImpl implements UndoController {
|
|
|
927
1235
|
const scopePaths = [...(plan.scopePaths ?? [...plan.deletePaths, ...plan.writePaths])].sort();
|
|
928
1236
|
const payload = {
|
|
929
1237
|
schemaVersion: 1 as const,
|
|
930
|
-
opId: `op-${randomUUID()}`,
|
|
1238
|
+
opId: this.operationId ?? `op-${randomUUID()}`,
|
|
931
1239
|
sessionIdentity: this.dependencies.sessionIdentity,
|
|
932
1240
|
workspaceIdentity: this.dependencies.workspaceIdentity,
|
|
933
1241
|
action,
|
|
@@ -1002,7 +1310,11 @@ function emptyRestorePlan(currentManifestId: ManifestId, targetManifestId: Manif
|
|
|
1002
1310
|
class OperationProfiler {
|
|
1003
1311
|
private readonly durations = new Map<string, number>();
|
|
1004
1312
|
|
|
1313
|
+
constructor(private readonly onProgress?: (phase: string) => void) {}
|
|
1314
|
+
|
|
1005
1315
|
async measure<T>(phase: string, operation: () => Promise<T>): Promise<T> {
|
|
1316
|
+
if (phase !== "unlock" && phase !== "commit" && !phase.startsWith("settled.")) checkOperation();
|
|
1317
|
+
this.onProgress?.(phase);
|
|
1006
1318
|
const started = performance.now();
|
|
1007
1319
|
try {
|
|
1008
1320
|
return await operation();
|
|
@@ -1024,6 +1336,26 @@ class OperationProfiler {
|
|
|
1024
1336
|
}
|
|
1025
1337
|
}
|
|
1026
1338
|
|
|
1339
|
+
async function waitForCompletion(task: Promise<void>, deadline: number): Promise<boolean> {
|
|
1340
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1341
|
+
const signal = currentOperationContext()?.signal;
|
|
1342
|
+
let onAbort: (() => void) | undefined;
|
|
1343
|
+
try {
|
|
1344
|
+
checkOperation();
|
|
1345
|
+
return await Promise.race([
|
|
1346
|
+
task.then(() => true, () => false),
|
|
1347
|
+
new Promise<boolean>((resolve) => {
|
|
1348
|
+
timer = setTimeout(() => resolve(false), Math.max(0, deadline - Date.now()));
|
|
1349
|
+
onAbort = () => resolve(false);
|
|
1350
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1351
|
+
}),
|
|
1352
|
+
]);
|
|
1353
|
+
} finally {
|
|
1354
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
1355
|
+
if (onAbort !== undefined) signal?.removeEventListener("abort", onAbort);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1027
1359
|
function noop(): OperationResult {
|
|
1028
1360
|
return { code: "noop", changedFiles: 0 };
|
|
1029
1361
|
}
|