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 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-rewind-hook",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "Rewind extension for Pi agent - automatic git checkpoints with file/conversation restore",
5
5
  "repository": {
6
6
  "type": "git",