pi-resume 1.0.0 → 1.0.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/LICENSE +0 -0
- package/README.md +26 -5
- package/extensions/index.ts +83 -18
- package/package.json +2 -2
- package/src/config.ts +0 -0
- package/src/format.ts +0 -0
- package/src/scanner.ts +102 -1
- package/src/session-dir.ts +0 -0
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -8,12 +8,13 @@ Built-in `/resume` reads and parses **every line** of every session file to buil
|
|
|
8
8
|
|
|
9
9
|
## Solution
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Commands that use `stat()` + lazy partial reads:
|
|
12
12
|
|
|
13
13
|
| Command | What it does | Speed |
|
|
14
14
|
|---------|-------------|-------|
|
|
15
|
-
| `/
|
|
15
|
+
| `/r1` … `/r5` | Instantly switch to the N-th most recent session (`/r1` = latest) | <50ms (stat-only) |
|
|
16
16
|
| `/rs` | Paginated picker: last 20, with tier navigation | <200ms first page |
|
|
17
|
+
| `/rds` | Delete subagent session trees for the current project (with confirmation) | — |
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -23,9 +24,17 @@ pi install npm:pi-fast-resume
|
|
|
23
24
|
|
|
24
25
|
## Commands
|
|
25
26
|
|
|
26
|
-
### `/
|
|
27
|
+
### `/r1` … `/r5` — Instant Ranked Resume
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Switch to the N-th most recent session (by mtime) in one step. No picker, no parsing.
|
|
30
|
+
|
|
31
|
+
- `/r1` — most recent session
|
|
32
|
+
- `/r2` — 2nd most recent
|
|
33
|
+
- … up to `/r5` — 5th most recent
|
|
34
|
+
|
|
35
|
+
The current session is always excluded from the ranking, so `/r1` reliably jumps
|
|
36
|
+
to the previous session. If fewer sessions exist than the requested rank, a
|
|
37
|
+
notice is shown and nothing is switched.
|
|
29
38
|
|
|
30
39
|
### `/rs` — Smart Resume
|
|
31
40
|
|
|
@@ -47,9 +56,21 @@ Auto-escalates: if 7d is empty, jumps to 14d, then all.
|
|
|
47
56
|
|
|
48
57
|
Config is stored in `~/.pi/agent/extensions/pi-fast-resume/config.json`.
|
|
49
58
|
|
|
59
|
+
### `/rds` — Delete Subagent Sessions
|
|
60
|
+
|
|
61
|
+
pi stores every subagent run under a subdirectory named like a top-level
|
|
62
|
+
session (`<timestamp>_<uuid>/`), containing `<runId>/run-N/session.jsonl`.
|
|
63
|
+
These accumulate on every subagent invocation and can bloat the sessions folder
|
|
64
|
+
by hundreds of MB.
|
|
65
|
+
|
|
66
|
+
`/rds` scans the **current project only**, shows how many trees / runs / MB
|
|
67
|
+
would be freed, asks for **confirmation**, then recursively deletes just those
|
|
68
|
+
subagent tree subdirectories. Your real top-level `*.jsonl` sessions (the ones
|
|
69
|
+
`/r1`…`/r5` and `/rs` list) are never touched.
|
|
70
|
+
|
|
50
71
|
## How it works
|
|
51
72
|
|
|
52
|
-
1. **`/
|
|
73
|
+
1. **`/r1`…`/r5`**: `readdir` → `stat` each `.jsonl` → sort by mtime → exclude current → `switchSession(others[rank-1])`
|
|
53
74
|
2. **`/rs`**: Same stat scan, then read only the **first ~50 lines** of each file on the current page to extract session name and first user message
|
|
54
75
|
|
|
55
76
|
No full file parsing. No `buildSessionInfo()`. No reading message content beyond the first user message.
|
package/extensions/index.ts
CHANGED
|
@@ -2,46 +2,111 @@
|
|
|
2
2
|
* pi-fast-resume — fast session resume without reading all .jsonl files.
|
|
3
3
|
*
|
|
4
4
|
* Commands:
|
|
5
|
-
* /
|
|
5
|
+
* /r1 .. /r5 — instantly switch to the N-th most recent session
|
|
6
|
+
* (stat-only; /r1 = latest, /r5 = 5th, current excluded)
|
|
6
7
|
* /rs — paginated session picker (last 20, "Load more", tier filter)
|
|
7
8
|
* /rs set page N — set page size (1-50)
|
|
8
9
|
* /rs set days N — set maxDays filter (0-30, 0 = no limit)
|
|
10
|
+
* /rds — delete all subagent session trees for the current project
|
|
11
|
+
* (asks for confirmation; top-level sessions untouched)
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
13
|
-
|
|
15
|
+
import {
|
|
16
|
+
statScan,
|
|
17
|
+
scanPage,
|
|
18
|
+
readSessionMeta,
|
|
19
|
+
scanSubagentTrees,
|
|
20
|
+
deleteSubagentTrees,
|
|
21
|
+
} from "../src/scanner.ts";
|
|
22
|
+
import { formatEntry, truncate, sessionLabel, formatSize } from "../src/format.ts";
|
|
14
23
|
import { loadConfig, saveConfig } from "../src/config.ts";
|
|
15
24
|
import { getSessionDir } from "../src/session-dir.ts";
|
|
16
25
|
|
|
17
26
|
const DAY_TIERS = [7, 14, 0] as const;
|
|
18
27
|
|
|
28
|
+
// How many ranked instant-resume commands to register: /r1 .. /rN.
|
|
29
|
+
const MAX_RANK = 5;
|
|
30
|
+
|
|
31
|
+
const ordinal = (n: number): string => {
|
|
32
|
+
if (n === 1) return "most recent";
|
|
33
|
+
const suffix = n === 2 ? "nd" : n === 3 ? "rd" : "th";
|
|
34
|
+
return `${n}${suffix} most recent`;
|
|
35
|
+
};
|
|
36
|
+
|
|
19
37
|
export default function (pi: ExtensionAPI) {
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
// Register /r1 .. /rN — each jumps to the rank-th most recent session
|
|
39
|
+
// (by mtime, current session excluded). /r1 = latest, /r2 = 2nd, etc.
|
|
40
|
+
for (let rank = 1; rank <= MAX_RANK; rank++) {
|
|
41
|
+
pi.registerCommand(`r${rank}`, {
|
|
42
|
+
description: `Instantly resume the ${ordinal(rank)} session`,
|
|
43
|
+
handler: async (_args: string, ctx: any) => {
|
|
44
|
+
const sessionDir = getSessionDir(ctx.cwd);
|
|
45
|
+
const files = await statScan(sessionDir);
|
|
46
|
+
|
|
47
|
+
if (files.length === 0) {
|
|
48
|
+
ctx.ui.notify("No sessions found", "error");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentFile = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
53
|
+
const others = files.filter((f: { file: string }) => f.file !== currentFile);
|
|
54
|
+
|
|
55
|
+
const target = others[rank - 1];
|
|
56
|
+
if (!target) {
|
|
57
|
+
ctx.ui.notify(
|
|
58
|
+
others.length === 0
|
|
59
|
+
? "No other sessions to resume"
|
|
60
|
+
: `Only ${others.length} other session${others.length === 1 ? "" : "s"} available`,
|
|
61
|
+
"info",
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const meta = await readSessionMeta(target.file, target);
|
|
67
|
+
await ctx.switchSession(target.file, {
|
|
68
|
+
withSession: async (newCtx: any) => {
|
|
69
|
+
newCtx.ui.notify(`Resumed: ${truncate(sessionLabel(meta), 50)}`, "info");
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pi.registerCommand("rds", {
|
|
77
|
+
description: "Delete subagent session trees for the current project (with confirmation)",
|
|
22
78
|
handler: async (_args: string, ctx: any) => {
|
|
23
79
|
const sessionDir = getSessionDir(ctx.cwd);
|
|
24
|
-
const
|
|
80
|
+
const trees = await scanSubagentTrees(sessionDir);
|
|
25
81
|
|
|
26
|
-
if (
|
|
27
|
-
ctx.ui.notify("No sessions
|
|
82
|
+
if (trees.length === 0) {
|
|
83
|
+
ctx.ui.notify("No subagent sessions to delete for this project", "info");
|
|
28
84
|
return;
|
|
29
85
|
}
|
|
30
86
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
87
|
+
const totalRuns = trees.reduce((s, t) => s + t.runs, 0);
|
|
88
|
+
const totalBytes = trees.reduce((s, t) => s + t.bytes, 0);
|
|
89
|
+
|
|
90
|
+
const summary =
|
|
91
|
+
`Delete ${trees.length} subagent tree${trees.length === 1 ? "" : "s"} ` +
|
|
92
|
+
`(${totalRuns} run${totalRuns === 1 ? "" : "s"}, ${formatSize(totalBytes)})?`;
|
|
93
|
+
|
|
94
|
+
const choice = await ctx.ui.select(summary, [
|
|
95
|
+
`Delete ${trees.length} tree${trees.length === 1 ? "" : "s"} (${formatSize(totalBytes)})`,
|
|
96
|
+
"Cancel",
|
|
97
|
+
]);
|
|
33
98
|
|
|
34
|
-
if (!
|
|
35
|
-
ctx.ui.notify("
|
|
99
|
+
if (!choice || choice === "Cancel") {
|
|
100
|
+
ctx.ui.notify("Cancelled — nothing deleted", "info");
|
|
36
101
|
return;
|
|
37
102
|
}
|
|
38
103
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
104
|
+
const removed = await deleteSubagentTrees(sessionDir, trees);
|
|
105
|
+
ctx.ui.notify(
|
|
106
|
+
`Deleted ${removed}/${trees.length} subagent tree${removed === 1 ? "" : "s"} ` +
|
|
107
|
+
`(${formatSize(totalBytes)} freed)`,
|
|
108
|
+
removed === trees.length ? "info" : "error",
|
|
109
|
+
);
|
|
45
110
|
},
|
|
46
111
|
});
|
|
47
112
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-resume",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Fast session resume for pi coding agent — /
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Fast session resume for pi coding agent — /r1../r5 ranked instant resume, /rs paginated picker, /rds subagent session cleanup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
package/src/config.ts
CHANGED
|
File without changes
|
package/src/format.ts
CHANGED
|
File without changes
|
package/src/scanner.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - First ~50 lines for header, session_info, first user message
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readdir, stat } from "node:fs/promises";
|
|
9
|
+
import { readdir, stat, rm } from "node:fs/promises";
|
|
10
10
|
import { createReadStream } from "node:fs";
|
|
11
11
|
import { createInterface } from "node:readline";
|
|
12
12
|
import { join } from "node:path";
|
|
@@ -165,3 +165,104 @@ export async function scanPage(
|
|
|
165
165
|
|
|
166
166
|
return { entries, total, hasMore };
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
// A session-tree subdir is named exactly like a top-level session file, minus
|
|
170
|
+
// the `.jsonl` suffix: `<ISO-timestamp>_<uuid>`. pi nests every subagent run
|
|
171
|
+
// (`<runId>/run-N/session.jsonl`) under such a directory. We match that shape
|
|
172
|
+
// so we never touch unrelated folders.
|
|
173
|
+
const SESSION_TREE_RE = /^\d{4}-\d{2}-\d{2}T[\dZ.-]+_[0-9a-f-]{8,}$/i;
|
|
174
|
+
|
|
175
|
+
export interface SubagentTree {
|
|
176
|
+
dir: string; // absolute path to the <timestamp>_<uuid> subdir
|
|
177
|
+
name: string; // the directory basename
|
|
178
|
+
runs: number; // count of run-*/session.jsonl files under it
|
|
179
|
+
bytes: number; // total size of the tree on disk
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Find subagent session trees for a project dir.
|
|
184
|
+
*
|
|
185
|
+
* pi stores every subagent run under a subdirectory named like a top-level
|
|
186
|
+
* session (`<timestamp>_<uuid>/`), containing `<runId>/run-N/session.jsonl`.
|
|
187
|
+
* These accumulate fast and bloat the sessions folder. This scans ONLY the
|
|
188
|
+
* given project dir (non-recursive at the top), returning each matching
|
|
189
|
+
* subdir with its run count and on-disk size. Top-level `*.jsonl` session
|
|
190
|
+
* files (the real user sessions) are never included.
|
|
191
|
+
*/
|
|
192
|
+
export async function scanSubagentTrees(sessionDir: string): Promise<SubagentTree[]> {
|
|
193
|
+
let entries: string[];
|
|
194
|
+
try {
|
|
195
|
+
entries = await readdir(sessionDir, { withFileTypes: true }) as any;
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const dirs = (entries as unknown as { name: string; isDirectory: () => boolean }[])
|
|
201
|
+
.filter((e) => e.isDirectory() && SESSION_TREE_RE.test(e.name))
|
|
202
|
+
.map((e) => e.name);
|
|
203
|
+
|
|
204
|
+
const trees = await Promise.all(
|
|
205
|
+
dirs.map(async (name) => {
|
|
206
|
+
const dir = join(sessionDir, name);
|
|
207
|
+
const { runs, bytes } = await measureTree(dir);
|
|
208
|
+
return { dir, name, runs, bytes };
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Newest first (by directory-name timestamp, which sorts lexicographically).
|
|
213
|
+
trees.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0));
|
|
214
|
+
return trees;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Recursively count `session.jsonl` (run-N) files and total bytes under a dir. */
|
|
218
|
+
async function measureTree(dir: string): Promise<{ runs: number; bytes: number }> {
|
|
219
|
+
let runs = 0;
|
|
220
|
+
let bytes = 0;
|
|
221
|
+
|
|
222
|
+
async function walk(d: string): Promise<void> {
|
|
223
|
+
let items: { name: string; isDirectory: () => boolean; isFile: () => boolean }[];
|
|
224
|
+
try {
|
|
225
|
+
items = (await readdir(d, { withFileTypes: true })) as any;
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
for (const it of items) {
|
|
230
|
+
const full = join(d, it.name);
|
|
231
|
+
if (it.isDirectory()) {
|
|
232
|
+
await walk(full);
|
|
233
|
+
} else if (it.isFile()) {
|
|
234
|
+
try {
|
|
235
|
+
const s = await stat(full);
|
|
236
|
+
bytes += s.size;
|
|
237
|
+
if (it.name === "session.jsonl") runs++;
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await walk(dir);
|
|
244
|
+
return { runs, bytes };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Delete a list of subagent tree directories (recursively). Returns the count
|
|
249
|
+
* successfully removed. Guards against deleting anything that isn't a
|
|
250
|
+
* session-tree subdir under the given sessionDir.
|
|
251
|
+
*/
|
|
252
|
+
export async function deleteSubagentTrees(
|
|
253
|
+
sessionDir: string,
|
|
254
|
+
trees: SubagentTree[],
|
|
255
|
+
): Promise<number> {
|
|
256
|
+
let removed = 0;
|
|
257
|
+
for (const t of trees) {
|
|
258
|
+
// Safety: the dir must live directly under sessionDir and match the shape.
|
|
259
|
+
if (join(sessionDir, t.name) !== t.dir || !SESSION_TREE_RE.test(t.name)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
await rm(t.dir, { recursive: true, force: true });
|
|
264
|
+
removed++;
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
return removed;
|
|
268
|
+
}
|
package/src/session-dir.ts
CHANGED
|
File without changes
|