@davideasden/pi-undo 0.2.1 → 0.2.2
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/package.json +1 -1
- package/src/controller.ts +111 -2
- package/src/pi-runtime.ts +9 -2
- package/src/recovery.ts +15 -13
package/package.json
CHANGED
package/src/controller.ts
CHANGED
|
@@ -406,6 +406,9 @@ export class UndoControllerImpl implements UndoController {
|
|
|
406
406
|
} catch {
|
|
407
407
|
return done({ code: "busy", changedFiles: 0 });
|
|
408
408
|
}
|
|
409
|
+
if (checkpoint.changedPaths.length === 0) {
|
|
410
|
+
return done(await this.runSessionOnlyOperation(action, checkpoint, targetManifestId, profile));
|
|
411
|
+
}
|
|
409
412
|
let rollback: SnapshotManifest;
|
|
410
413
|
try {
|
|
411
414
|
rollback = await profile.measure("capture", () =>
|
|
@@ -492,6 +495,84 @@ export class UndoControllerImpl implements UndoController {
|
|
|
492
495
|
}
|
|
493
496
|
}
|
|
494
497
|
|
|
498
|
+
private async runSessionOnlyOperation(
|
|
499
|
+
action: "undo" | "redo",
|
|
500
|
+
checkpoint: CheckpointRecord,
|
|
501
|
+
targetManifestId: ManifestId | undefined,
|
|
502
|
+
profile: OperationProfiler,
|
|
503
|
+
): Promise<OperationResult> {
|
|
504
|
+
const rollbackManifestId = action === "undo"
|
|
505
|
+
? checkpoint.afterManifestId
|
|
506
|
+
: checkpoint.beforeManifestId;
|
|
507
|
+
const targetId = targetManifestId ?? (
|
|
508
|
+
action === "undo" ? checkpoint.beforeManifestId : checkpoint.afterManifestId
|
|
509
|
+
);
|
|
510
|
+
const plan = emptyRestorePlan(rollbackManifestId, targetId);
|
|
511
|
+
const targetLogicalLeaf = this.dependencies.resolveSessionTarget(action, checkpoint);
|
|
512
|
+
const descriptor = this.createDescriptorFromManifestIds(
|
|
513
|
+
action,
|
|
514
|
+
rollbackManifestId,
|
|
515
|
+
targetId,
|
|
516
|
+
plan,
|
|
517
|
+
targetLogicalLeaf,
|
|
518
|
+
);
|
|
519
|
+
await profile.measure("journal", () => this.dependencies.journal.prepare(descriptor, plan));
|
|
520
|
+
const navigation = await profile.measure("navigate", () =>
|
|
521
|
+
this.dependencies.navigateSession(action, checkpoint));
|
|
522
|
+
if (navigation.cancelled) {
|
|
523
|
+
await profile.measure("journal", async () => {
|
|
524
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTING");
|
|
525
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTED");
|
|
526
|
+
});
|
|
527
|
+
return { code: "restore_failed_safe", changedFiles: 0 };
|
|
528
|
+
}
|
|
529
|
+
if (navigation.logicalLeafId !== descriptor.toLogicalLeaf) {
|
|
530
|
+
this.locked = true;
|
|
531
|
+
await profile.measure("journal", () =>
|
|
532
|
+
this.dependencies.journal.setPhase(descriptor.opId, "RECOVERY_REQUIRED"));
|
|
533
|
+
return { code: "recovery_required", changedFiles: 0 };
|
|
534
|
+
}
|
|
535
|
+
await profile.measure("journal", async () => {
|
|
536
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "SESSION_MOVED", {
|
|
537
|
+
observedLogicalLeaf: navigation.logicalLeafId,
|
|
538
|
+
});
|
|
539
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "APPLYING");
|
|
540
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "FILES_VERIFIED");
|
|
541
|
+
});
|
|
542
|
+
const cursorResult = await profile.measure("cursor", () =>
|
|
543
|
+
this.dependencies.appendCursor(this.createCursor(descriptor, action, checkpoint)));
|
|
544
|
+
if (cursorResult.kind === "recovery_required") {
|
|
545
|
+
this.locked = true;
|
|
546
|
+
await profile.measure("journal", () =>
|
|
547
|
+
this.dependencies.journal.setPhase(descriptor.opId, "RECOVERY_REQUIRED").catch(() => {}));
|
|
548
|
+
return { code: "recovery_required", changedFiles: 0 };
|
|
549
|
+
}
|
|
550
|
+
if (cursorResult.kind === "volatile") {
|
|
551
|
+
return profile.measure("compensate", () => this.compensateSessionOnly(descriptor));
|
|
552
|
+
}
|
|
553
|
+
await profile.measure("commit", async () => {
|
|
554
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "CURSOR_COMMITTED");
|
|
555
|
+
await this.dependencies.journal.markCommitted(descriptor.opId);
|
|
556
|
+
});
|
|
557
|
+
this.lastSafetyManifestId = rollbackManifestId;
|
|
558
|
+
return { code: "ok", changedFiles: 0 };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private async compensateSessionOnly(descriptor: OperationDescriptor): Promise<OperationResult> {
|
|
562
|
+
try {
|
|
563
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTING");
|
|
564
|
+
if (!await this.dependencies.restoreSessionLeaf(descriptor.fromLogicalLeaf)) {
|
|
565
|
+
throw new Error("session rollback failed");
|
|
566
|
+
}
|
|
567
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "ABORTED");
|
|
568
|
+
return { code: "restore_failed_safe", changedFiles: 0 };
|
|
569
|
+
} catch {
|
|
570
|
+
this.locked = true;
|
|
571
|
+
await this.dependencies.journal.setPhase(descriptor.opId, "RECOVERY_REQUIRED").catch(() => {});
|
|
572
|
+
return { code: "recovery_required", changedFiles: 0 };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
495
576
|
private async ensureIdle(): Promise<boolean> {
|
|
496
577
|
if (this.dependencies.isAgentIdle()) return true;
|
|
497
578
|
try {
|
|
@@ -569,6 +650,22 @@ export class UndoControllerImpl implements UndoController {
|
|
|
569
650
|
target: SnapshotManifest,
|
|
570
651
|
plan: RestorePlan,
|
|
571
652
|
targetLogicalLeaf: string | null,
|
|
653
|
+
): OperationDescriptor {
|
|
654
|
+
return this.createDescriptorFromManifestIds(
|
|
655
|
+
action,
|
|
656
|
+
rollback.manifestId,
|
|
657
|
+
target.manifestId,
|
|
658
|
+
plan,
|
|
659
|
+
targetLogicalLeaf,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private createDescriptorFromManifestIds(
|
|
664
|
+
action: "undo" | "redo" | "tree",
|
|
665
|
+
rollbackManifestId: ManifestId,
|
|
666
|
+
targetManifestId: ManifestId,
|
|
667
|
+
plan: RestorePlan,
|
|
668
|
+
targetLogicalLeaf: string | null,
|
|
572
669
|
): OperationDescriptor {
|
|
573
670
|
const scopePaths = [...(plan.scopePaths ?? [...plan.deletePaths, ...plan.writePaths])].sort();
|
|
574
671
|
const payload = {
|
|
@@ -579,8 +676,8 @@ export class UndoControllerImpl implements UndoController {
|
|
|
579
676
|
action,
|
|
580
677
|
fromLogicalLeaf: this.dependencies.getLogicalLeafId(),
|
|
581
678
|
toLogicalLeaf: targetLogicalLeaf,
|
|
582
|
-
targetManifestId
|
|
583
|
-
rollbackManifestId
|
|
679
|
+
targetManifestId,
|
|
680
|
+
rollbackManifestId,
|
|
584
681
|
coverage: `paths:${checksum(canonicalJson(scopePaths))}`,
|
|
585
682
|
scopePaths,
|
|
586
683
|
planDigest: plan.planDigest,
|
|
@@ -633,6 +730,18 @@ export class UndoControllerImpl implements UndoController {
|
|
|
633
730
|
}
|
|
634
731
|
}
|
|
635
732
|
|
|
733
|
+
function emptyRestorePlan(currentManifestId: ManifestId, targetManifestId: ManifestId): RestorePlan {
|
|
734
|
+
const payload = {
|
|
735
|
+
currentManifestId,
|
|
736
|
+
targetManifestId,
|
|
737
|
+
boundaryRoots: [],
|
|
738
|
+
deletePaths: [],
|
|
739
|
+
writePaths: [],
|
|
740
|
+
scopePaths: [],
|
|
741
|
+
};
|
|
742
|
+
return { ...payload, planDigest: checksum(canonicalJson(payload)) };
|
|
743
|
+
}
|
|
744
|
+
|
|
636
745
|
class OperationProfiler {
|
|
637
746
|
private readonly durations = new Map<string, number>();
|
|
638
747
|
|
package/src/pi-runtime.ts
CHANGED
|
@@ -59,8 +59,15 @@ export async function createPiUndoRuntime(context: ExtensionContext, pi: Extensi
|
|
|
59
59
|
recoverMutations: async (pending, decision) => {
|
|
60
60
|
const mutationJournal = journal.mutationJournal(pending.descriptor.opId);
|
|
61
61
|
const quarantine = new QuarantineManager({ workspaceRoot: context.cwd, journal: mutationJournal });
|
|
62
|
-
|
|
63
|
-
const
|
|
62
|
+
try {
|
|
63
|
+
const loaded = await mutationJournal.load();
|
|
64
|
+
const scopePaths = pending.descriptor.scopePaths;
|
|
65
|
+
if (loaded.some((record) => !scopePaths.some((scope) =>
|
|
66
|
+
scope === "." || record.path === scope || record.path.startsWith(`${scope}/`)
|
|
67
|
+
))) {
|
|
68
|
+
throw new Error("mutation journal path 超出 descriptor scope");
|
|
69
|
+
}
|
|
70
|
+
const records = loaded.filter((record) => record.state !== "CLEANED");
|
|
64
71
|
if (decision === "rollback") records.reverse();
|
|
65
72
|
for (const record of records) {
|
|
66
73
|
if (decision === "rollback") {
|
package/src/recovery.ts
CHANGED
|
@@ -100,19 +100,21 @@ export class JournalRecovery {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
if (journal.descriptor.scopePaths.length > 0) {
|
|
104
|
+
const current = await this.dependencies.capture();
|
|
105
|
+
const targetId = inspection.kind === "match"
|
|
106
|
+
? journal.descriptor.targetManifestId
|
|
107
|
+
: journal.descriptor.rollbackManifestId;
|
|
108
|
+
const target = await this.dependencies.loadManifest(targetId);
|
|
109
|
+
const plan = await this.dependencies.planRestore(current, target, journal.descriptor.scopePaths);
|
|
110
|
+
const applied = await this.dependencies.applyRestore(
|
|
111
|
+
plan,
|
|
112
|
+
target,
|
|
113
|
+
{ opId: journal.descriptor.opId },
|
|
114
|
+
);
|
|
115
|
+
if (applied.code !== "ok") {
|
|
116
|
+
return { kind: "locked", reason: "restore_failed", operations: recovered };
|
|
117
|
+
}
|
|
116
118
|
}
|
|
117
119
|
if (inspection.kind === "match") {
|
|
118
120
|
await this.dependencies.finalizeCursor(journal, inspection);
|