@saifymatteo/pi-session-retention 0.1.3 → 0.1.5

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 ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ Release notes are recorded from 0.1.3 onward.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.5]
8
+
9
+ ### Added
10
+
11
+ - Include package release notes in Pi 1.5.0's Extension Updates.
12
+
13
+ ## [0.1.4]
14
+
15
+ ### Fixed
16
+
17
+ - Schedule automatic maintenance outside the awaited session-start handler; defer launch while the agent is busy.
18
+ - Offer `/session-cleanup` for first approval without interrupting startup, retaining store locking, protected sessions, and shutdown draining.
19
+
20
+ ## [0.1.3]
21
+
22
+ ### Fixed
23
+
24
+ - Classify the package as an extension in the Pi package gallery.
25
+ - Show the package screenshot as its gallery preview.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saifymatteo/pi-session-retention",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "src",
12
12
  "README.md",
13
+ "CHANGELOG.md",
13
14
  "NOTICE.md",
14
15
  "LICENSE"
15
16
  ],
package/src/index.ts CHANGED
@@ -121,6 +121,9 @@ async function trashSearchText(trash: string, batch: TrashBatch): Promise<{ sear
121
121
  export default function sessionRetention(pi: ExtensionAPI, options: { guard?: () => Promise<void>; now?: () => number } = {}) {
122
122
  const now = options.now ?? Date.now;
123
123
  let started = false;
124
+ let pendingAutomatic = false;
125
+ let automaticTimer: ReturnType<typeof setTimeout> | undefined;
126
+ let automatic: Promise<void> | undefined;
124
127
  let maintaining = false;
125
128
  let lifecycle = new AbortController();
126
129
  let operation: Promise<unknown> | undefined;
@@ -159,10 +162,18 @@ export default function sessionRetention(pi: ExtensionAPI, options: { guard?: ()
159
162
  };
160
163
  pi.on("session_before_switch", preventSwitch);
161
164
  pi.on("session_before_fork", preventSwitch);
165
+ function cancelAutomatic() {
166
+ pendingAutomatic = false;
167
+ if (automaticTimer !== undefined) clearTimeout(automaticTimer);
168
+ automaticTimer = undefined;
169
+ }
162
170
  pi.on("session_shutdown", async (_event, ctx) => {
171
+ cancelAutomatic();
163
172
  status(ctx, undefined);
164
173
  lifecycle.abort();
165
174
  closeDialog?.();
175
+ // Drain the background handler as well as file work before replacing its context.
176
+ await automatic?.catch(() => {});
166
177
  // Never release a lock while a file operation can still be using it.
167
178
  await operation?.catch(() => {});
168
179
  started = false;
@@ -199,7 +210,13 @@ export default function sessionRetention(pi: ExtensionAPI, options: { guard?: ()
199
210
  ].join("\n");
200
211
  if (previewOnly) { status(ctx, "Session cleanup: preview — no changes"); await ctx.ui.confirm("Preview only — nothing will be changed", summary, { signal }); return; }
201
212
  if (!state.approved || state.paused) {
202
- if (!manual && !count && !expired.length) { state.lastRun = time; await saveState(paths.trash, state); return; }
213
+ if (!manual) {
214
+ // Background discovery must not replace the editor with an approval dialog.
215
+ if (count || expired.length) ctx.ui.notify("Session cleanup is available. Run /session-cleanup to review and enable daily automatic cleanup.", "info");
216
+ state.lastRun = time;
217
+ await saveState(paths.trash, state);
218
+ return;
219
+ }
203
220
  status(ctx, "Session cleanup: awaiting approval…");
204
221
  const approved = await ctx.ui.confirm("Enable daily session cleanup?", `${summary}\n\nApprove this cleanup and future automatic cleanup at startup (at most daily)?`, { signal });
205
222
  signal.throwIfAborted();
@@ -228,13 +245,33 @@ export default function sessionRetention(pi: ExtensionAPI, options: { guard?: ()
228
245
  });
229
246
  }
230
247
 
231
- pi.on("session_start", async (_event, ctx) => {
248
+ function scheduleAutomatic(ctx: ExtensionContext) {
249
+ if (!pendingAutomatic || automaticTimer !== undefined || automatic || maintaining) return;
250
+ const signal = lifecycle.signal;
251
+ if (signal.aborted) return;
252
+ // Yield before starting maintenance; startup never awaits this task.
253
+ automaticTimer = setTimeout(() => {
254
+ automaticTimer = undefined;
255
+ if (signal.aborted || !pendingAutomatic || !ctx.isIdle() || maintaining) return;
256
+ pendingAutomatic = false;
257
+ automatic = cleanup(ctx, false)
258
+ .catch((error) => {
259
+ if (!signal.aborted) {
260
+ try { ctx.ui.notify(message(error), "warning"); } catch { /* UI failures must not reject a detached task. */ }
261
+ }
262
+ })
263
+ .finally(() => { automatic = undefined; });
264
+ }, 0);
265
+ automaticTimer.unref();
266
+ }
267
+ pi.on("session_start", (_event, ctx) => {
232
268
  if (lifecycle.signal.aborted) lifecycle = new AbortController();
233
269
  if (started || ctx.mode !== "tui" || !ctx.sessionManager.getSessionFile()) return;
234
270
  started = true;
235
- const signal = lifecycle.signal;
236
- try { await cleanup(ctx, false); } catch (error) { if (!signal.aborted) ctx.ui.notify(message(error), "warning"); }
271
+ pendingAutomatic = true;
272
+ scheduleAutomatic(ctx);
237
273
  });
274
+ pi.on("agent_settled", (_event, ctx) => { scheduleAutomatic(ctx); });
238
275
 
239
276
  pi.registerCommand("session-cleanup", {
240
277
  description: "Clean inactive session families [preview|pause]",
@@ -250,6 +287,7 @@ export default function sessionRetention(pi: ExtensionAPI, options: { guard?: ()
250
287
  const signal = lifecycle.signal;
251
288
  const action = args.trim();
252
289
  if (!["", "preview", "pause"].includes(action)) return ctx.ui.notify("Usage: /session-cleanup [preview|pause]", "info");
290
+ cancelAutomatic();
253
291
  try {
254
292
  if (action === "pause") {
255
293
  const paths = storePaths(ctx);
@@ -265,6 +303,7 @@ export default function sessionRetention(pi: ExtensionAPI, options: { guard?: ()
265
303
  description: "Search Trash by names, first prompts, IDs or paths; restore a whole family",
266
304
  handler: async (args, ctx) => {
267
305
  if (ctx.mode !== "tui") return ctx.ui.notify("Session Trash requires interactive mode.", "warning");
306
+ cancelAutomatic();
268
307
  const signal = lifecycle.signal;
269
308
  try {
270
309
  await ctx.waitForIdle();