pi-rewind-hook 1.8.3 → 1.8.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/CHANGELOG.md +8 -0
- package/README.md +12 -0
- package/index.test.ts +75 -0
- package/index.ts +36 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.8.4] - 2026-04-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added `pi-custom-compaction` compatibility: custom message nodes can now receive exact Rewind checkpoints via the `rewind:checkpoint-entry` extension event.
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
- Documented seamless `pi-custom-compaction` compatibility and the `pi install npm:pi-custom-compaction` install command.
|
|
14
|
+
|
|
7
15
|
## [1.8.3] - 2026-04-16
|
|
8
16
|
|
|
9
17
|
### Fixed
|
package/README.md
CHANGED
|
@@ -29,6 +29,18 @@ This will:
|
|
|
29
29
|
4. Remove legacy hooks/explicit rewind extension entries from `settings.json` if present
|
|
30
30
|
5. Clean up old `hooks/rewind` directory (if present)
|
|
31
31
|
|
|
32
|
+
## Works with pi-custom-compaction
|
|
33
|
+
|
|
34
|
+
Rewind works seamlessly with [`pi-custom-compaction`](https://github.com/nicobailon/pi-custom-compaction). When `pi-custom-compaction` writes visible background-summary markers, it can ask Rewind to bind an exact file checkpoint to that marker’s own session-tree node. That means `/tree` can offer the normal `Restore files to that point` option directly on the custom compaction marker instead of falling back to a nearby parent node.
|
|
35
|
+
|
|
36
|
+
Install the companion extension with:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pi install npm:pi-custom-compaction
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This is optional. Pi’s default compaction and Rewind’s native compaction checkpoints continue to work without `pi-custom-compaction`.
|
|
43
|
+
|
|
32
44
|
## Configuration
|
|
33
45
|
|
|
34
46
|
Optionally add settings to `~/.pi/agent/settings.json`. For example:
|
package/index.test.ts
CHANGED
|
@@ -438,6 +438,81 @@ test("session_before_tree gracefully cancels when restore fails", async () => {
|
|
|
438
438
|
}
|
|
439
439
|
});
|
|
440
440
|
|
|
441
|
+
test("session_before_tree restores directly from a custom message with a checkpoint", async () => {
|
|
442
|
+
const harness = await createHarness({
|
|
443
|
+
settings: { rewind: { silentCheckpoints: true } },
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await harness.writeRepoFile("notes.txt", "target state\n");
|
|
448
|
+
const targetCommit = await harness.captureSnapshot();
|
|
449
|
+
await harness.writeRepoFile("notes.txt", "current state\n");
|
|
450
|
+
|
|
451
|
+
harness.currentSession.replaceEntries([
|
|
452
|
+
{
|
|
453
|
+
type: "custom_message",
|
|
454
|
+
id: "marker-1",
|
|
455
|
+
parentId: null,
|
|
456
|
+
timestamp: new Date().toISOString(),
|
|
457
|
+
customType: "pi-custom-compaction.virtual-summary-marker",
|
|
458
|
+
content: "Older context was summarized in the background.",
|
|
459
|
+
display: true,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
type: "custom",
|
|
463
|
+
id: "rewind-op-1",
|
|
464
|
+
parentId: "marker-1",
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
customType: "rewind-op",
|
|
467
|
+
data: { v: 2, snapshots: [targetCommit], bindings: [["marker-1", 0]] },
|
|
468
|
+
},
|
|
469
|
+
]);
|
|
470
|
+
|
|
471
|
+
await harness.invoke("session_start", {});
|
|
472
|
+
harness.enqueueSelection("Restore files to that point");
|
|
473
|
+
|
|
474
|
+
const result = await harness.invoke("session_before_tree", { preparation: { targetId: "marker-1" } });
|
|
475
|
+
assert.equal(result, undefined);
|
|
476
|
+
assert.equal(harness.readRepoFile("notes.txt"), "target state\n");
|
|
477
|
+
assert.deepEqual(harness.selectCalls[0]?.options, ["Keep current files", "Restore files to that point", "Cancel navigation"]);
|
|
478
|
+
} finally {
|
|
479
|
+
await harness.cleanup();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("rewind:checkpoint-entry binds the current tree to a custom message", async () => {
|
|
484
|
+
const harness = await createHarness({
|
|
485
|
+
settings: { rewind: { silentCheckpoints: true } },
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
await harness.writeRepoFile("notes.txt", "target state\n");
|
|
490
|
+
harness.currentSession.replaceEntries([
|
|
491
|
+
{
|
|
492
|
+
type: "custom_message",
|
|
493
|
+
id: "marker-1",
|
|
494
|
+
parentId: null,
|
|
495
|
+
timestamp: new Date().toISOString(),
|
|
496
|
+
customType: "pi-custom-compaction.virtual-summary-marker",
|
|
497
|
+
content: "Older context was summarized in the background.",
|
|
498
|
+
display: true,
|
|
499
|
+
},
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
await harness.invoke("session_start", {});
|
|
503
|
+
harness.eventHandlers.get("rewind:checkpoint-entry")?.({ source: "pi-custom-compaction", entryId: "marker-1" });
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
505
|
+
await harness.writeRepoFile("notes.txt", "current state\n");
|
|
506
|
+
|
|
507
|
+
harness.enqueueSelection("Restore files to that point");
|
|
508
|
+
const result = await harness.invoke("session_before_tree", { preparation: { targetId: "marker-1" } });
|
|
509
|
+
assert.equal(result, undefined);
|
|
510
|
+
assert.equal(harness.readRepoFile("notes.txt"), "target state\n");
|
|
511
|
+
} finally {
|
|
512
|
+
await harness.cleanup();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
441
516
|
test("session_before_tree auto-keeps current files during boomerang collapse", async () => {
|
|
442
517
|
const harness = await createHarness({
|
|
443
518
|
settings: { rewind: { silentCheckpoints: true } },
|
package/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ const execAsync = promisify(execCb);
|
|
|
11
11
|
const STORE_REF = "refs/pi-rewind/store";
|
|
12
12
|
const STATUS_KEY = "rewind";
|
|
13
13
|
const FORK_PREFERENCE_SOURCE_ALLOWLIST = new Set(["fork-from-first"]);
|
|
14
|
+
const CHECKPOINT_SOURCE_ALLOWLIST = new Set(["pi-custom-compaction"]);
|
|
14
15
|
const LEGACY_ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
15
16
|
const RETENTION_SWEEP_THRESHOLD = 50;
|
|
16
17
|
const RETENTION_VERSION = 2;
|
|
@@ -316,7 +317,7 @@ function isRestorableTreeEntry(entry: SessionLikeEntry | undefined): boolean {
|
|
|
316
317
|
if (entry.type === "message") {
|
|
317
318
|
return entry.message.role === "user" || entry.message.role === "assistant";
|
|
318
319
|
}
|
|
319
|
-
return entry.type === "branch_summary" || entry.type === "compaction";
|
|
320
|
+
return entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "custom_message";
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
function isAssistantMessageEntry(entry: SessionLikeEntry): entry is SessionLikeMessageEntry {
|
|
@@ -1088,7 +1089,27 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1088
1089
|
updateStatus(ctx);
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1092
|
+
let activeContext: ExtensionContext | undefined;
|
|
1093
|
+
|
|
1094
|
+
async function checkpointEntry(ctx: ExtensionContext, entryId: string) {
|
|
1095
|
+
syncSessionIdentity(ctx);
|
|
1096
|
+
if (!isGitRepo) return;
|
|
1097
|
+
const entry = ctx.sessionManager.getEntry(entryId) as SessionLikeEntry | undefined;
|
|
1098
|
+
if (!isRestorableTreeEntry(entry)) return;
|
|
1099
|
+
if (entryToCommit.has(entryId)) return;
|
|
1100
|
+
|
|
1101
|
+
const currentCommitSha = await ensureSnapshotForCurrentWorktree();
|
|
1102
|
+
appendRewindOp(ctx, {
|
|
1103
|
+
v: RETENTION_VERSION,
|
|
1104
|
+
snapshots: [currentCommitSha],
|
|
1105
|
+
bindings: [[entryId, 0]],
|
|
1106
|
+
});
|
|
1107
|
+
await reconstructState(ctx);
|
|
1108
|
+
updateStatus(ctx);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1091
1111
|
async function initializeForSession(ctx: ExtensionContext) {
|
|
1112
|
+
activeContext = ctx;
|
|
1092
1113
|
resetState();
|
|
1093
1114
|
syncSessionIdentity(ctx);
|
|
1094
1115
|
|
|
@@ -1122,6 +1143,19 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1122
1143
|
forceConversationOnlySource = data.source;
|
|
1123
1144
|
});
|
|
1124
1145
|
|
|
1146
|
+
pi.events.on("rewind:checkpoint-entry", (data) => {
|
|
1147
|
+
if (!data || typeof data !== "object") return;
|
|
1148
|
+
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1149
|
+
if (!CHECKPOINT_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1150
|
+
if (!("entryId" in data) || typeof data.entryId !== "string") return;
|
|
1151
|
+
const ctx = activeContext;
|
|
1152
|
+
if (!ctx) return;
|
|
1153
|
+
|
|
1154
|
+
checkpointEntry(ctx, data.entryId).catch((error) => {
|
|
1155
|
+
notify(ctx, `Rewind: failed to checkpoint ${data.entryId} (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1125
1159
|
pi.on("before_agent_start", async (event) => {
|
|
1126
1160
|
activePromptText = event.prompt;
|
|
1127
1161
|
});
|
|
@@ -1392,7 +1426,7 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1392
1426
|
}
|
|
1393
1427
|
|
|
1394
1428
|
if (!targetCommitSha) {
|
|
1395
|
-
notify(ctx, "Exact file rewind is only available for user, assistant, compaction, and summary nodes", "error");
|
|
1429
|
+
notify(ctx, "Exact file rewind is only available for user, assistant, custom message, compaction, and summary nodes", "error");
|
|
1396
1430
|
return { cancel: true };
|
|
1397
1431
|
}
|
|
1398
1432
|
|