@saifymatteo/pi-session-retention 0.1.3 → 0.1.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/package.json +1 -1
- package/src/index.ts +43 -4
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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();
|