pi-blackhole 0.4.2 → 0.4.3
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/README.md +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
package/src/om/cleanup.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* session JSONL files to find orphaned entries safe to delete.
|
|
4
4
|
*
|
|
5
5
|
* Per-session pending files under ~/.pi/agent/pi-blackhole/ accumulate when:
|
|
6
|
-
* - compaction is set to "manual"
|
|
6
|
+
* - compaction is set to "manual" — OM outputs are
|
|
7
7
|
* buffered rather than appended to the session
|
|
8
8
|
* - sessions are forked, abandoned, or deleted — the pending files remain
|
|
9
9
|
* - stale backup files (-pending.stale.json) persist after write-safe renames
|
|
@@ -11,30 +11,36 @@
|
|
|
11
11
|
* Safety invariant: a pending file is ONLY orphaned if its sessionId does NOT
|
|
12
12
|
* appear in ANY session JSONL file across all known session directories.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
existsSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
statSync,
|
|
19
|
+
unlinkSync,
|
|
20
|
+
} from "node:fs";
|
|
15
21
|
import { join, resolve, sep } from "node:path";
|
|
16
22
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
17
23
|
|
|
18
24
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
19
25
|
|
|
20
26
|
export interface PendingFile {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
sessionId: string;
|
|
28
|
+
filename: string;
|
|
29
|
+
path: string;
|
|
30
|
+
/** True for -pending.stale.json (backup of previous write). */
|
|
31
|
+
isStale: boolean;
|
|
32
|
+
sizeBytes: number;
|
|
33
|
+
/** Last modified timestamp (epoch ms). */
|
|
34
|
+
mtimeMs: number;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export interface CleanupReport {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
/** All pending files found (both pending and stale). */
|
|
39
|
+
all: PendingFile[];
|
|
40
|
+
/** Files whose sessionId does not appear in any session JSONL. */
|
|
41
|
+
orphaned: PendingFile[];
|
|
42
|
+
/** Files whose sessionId was matched to an active session. */
|
|
43
|
+
active: PendingFile[];
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -46,13 +52,13 @@ const STALE_SUFFIX = "-pending.stale.json";
|
|
|
46
52
|
// ── Helper: extract session ID from filename ────────────────────────────────
|
|
47
53
|
|
|
48
54
|
function extractSessionId(filename: string): string | null {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
if (filename.endsWith(STALE_SUFFIX)) {
|
|
56
|
+
return filename.slice(0, -STALE_SUFFIX.length) || null;
|
|
57
|
+
}
|
|
58
|
+
if (filename.endsWith(PENDING_SUFFIX)) {
|
|
59
|
+
return filename.slice(0, -PENDING_SUFFIX.length) || null;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
// ── Scan pending files ──────────────────────────────────────────────────────
|
|
@@ -62,42 +68,42 @@ function extractSessionId(filename: string): string | null {
|
|
|
62
68
|
* files. Returns file metadata sorted by mtime (newest first).
|
|
63
69
|
*/
|
|
64
70
|
function scanPendingFiles(agentDir: string = getAgentDir()): PendingFile[] {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
71
|
+
const dir = join(agentDir, PENDING_DIR);
|
|
72
|
+
if (!existsSync(dir)) return [];
|
|
73
|
+
|
|
74
|
+
const results: PendingFile[] = [];
|
|
75
|
+
let entries: string[];
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const filename of entries) {
|
|
83
|
+
const sessionId = extractSessionId(filename);
|
|
84
|
+
if (!sessionId) continue;
|
|
85
|
+
|
|
86
|
+
const filePath = join(dir, filename);
|
|
87
|
+
let stat;
|
|
88
|
+
try {
|
|
89
|
+
stat = statSync(filePath);
|
|
90
|
+
} catch {
|
|
91
|
+
continue; // file disappeared between readdir and stat
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
results.push({
|
|
95
|
+
sessionId,
|
|
96
|
+
filename,
|
|
97
|
+
path: filePath,
|
|
98
|
+
isStale: filename.endsWith(STALE_SUFFIX),
|
|
99
|
+
sizeBytes: stat.size,
|
|
100
|
+
mtimeMs: stat.mtimeMs,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort: newest first
|
|
105
|
+
results.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
106
|
+
return results;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
// ── Scan session IDs from JSONL files ───────────────────────────────────────
|
|
@@ -107,47 +113,51 @@ function scanPendingFiles(agentDir: string = getAgentDir()): PendingFile[] {
|
|
|
107
113
|
* their header line (first line: {"type":"session","id":"<uuid>",...}).
|
|
108
114
|
*/
|
|
109
115
|
function scanSessionDir(dir: string): Set<string> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
116
|
+
const ids = new Set<string>();
|
|
117
|
+
if (!existsSync(dir)) return ids;
|
|
118
|
+
|
|
119
|
+
const stack: string[] = [dir];
|
|
120
|
+
while (stack.length > 0) {
|
|
121
|
+
const current = stack.pop()!;
|
|
122
|
+
let names: string[];
|
|
123
|
+
try {
|
|
124
|
+
names = readdirSync(current);
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
const fullPath = join(current, name);
|
|
131
|
+
let st;
|
|
132
|
+
try {
|
|
133
|
+
st = statSync(fullPath);
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (st.isDirectory()) {
|
|
139
|
+
stack.push(fullPath);
|
|
140
|
+
} else if (st.isFile() && name.endsWith(".jsonl")) {
|
|
141
|
+
try {
|
|
142
|
+
const fd = readFileSync(fullPath, "utf-8");
|
|
143
|
+
const newlineIdx = fd.indexOf("\n");
|
|
144
|
+
const firstLine = newlineIdx >= 0 ? fd.slice(0, newlineIdx) : fd;
|
|
145
|
+
const header = JSON.parse(firstLine) as Record<string, unknown>;
|
|
146
|
+
if (
|
|
147
|
+
header.type === "session" &&
|
|
148
|
+
typeof header.id === "string" &&
|
|
149
|
+
header.id.length > 0
|
|
150
|
+
) {
|
|
151
|
+
ids.add(header.id);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Corrupt or unreadable file — skip
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return ids;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
/**
|
|
@@ -155,20 +165,25 @@ function scanSessionDir(dir: string): Set<string> {
|
|
|
155
165
|
* Returns the resolved absolute path, or undefined if not set or unreadable.
|
|
156
166
|
*/
|
|
157
167
|
function readSettingsSessionDir(): string | undefined {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
try {
|
|
169
|
+
const settingsPath = join(getAgentDir(), "settings.json");
|
|
170
|
+
if (!existsSync(settingsPath)) return undefined;
|
|
171
|
+
const raw = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record<
|
|
172
|
+
string,
|
|
173
|
+
unknown
|
|
174
|
+
>;
|
|
175
|
+
const dir = raw.sessionDir;
|
|
176
|
+
if (typeof dir === "string" && dir.trim().length > 0) {
|
|
177
|
+
// Expand ~ if present
|
|
178
|
+
const expanded = dir.startsWith("~")
|
|
179
|
+
? join(process.env.HOME ?? "/home", dir.slice(2))
|
|
180
|
+
: dir;
|
|
181
|
+
return resolve(expanded);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Unreadable settings — use default
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
/**
|
|
@@ -178,20 +193,20 @@ function readSettingsSessionDir(): string | undefined {
|
|
|
178
193
|
*/
|
|
179
194
|
/** Get the default sessions directory. */
|
|
180
195
|
function getDefaultSessionsDir(): string {
|
|
181
|
-
|
|
196
|
+
return join(getAgentDir(), "sessions");
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
function findSessionDirs(): string[] {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
const dirs: string[] = [];
|
|
201
|
+
const default_ = getDefaultSessionsDir();
|
|
202
|
+
dirs.push(default_);
|
|
188
203
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
const custom = readSettingsSessionDir();
|
|
205
|
+
if (custom && custom !== default_ && existsSync(custom)) {
|
|
206
|
+
dirs.push(custom);
|
|
207
|
+
}
|
|
193
208
|
|
|
194
|
-
|
|
209
|
+
return dirs;
|
|
195
210
|
}
|
|
196
211
|
|
|
197
212
|
/**
|
|
@@ -199,13 +214,13 @@ function findSessionDirs(): string[] {
|
|
|
199
214
|
* Returns a Set of session UUIDs found in JSONL headers.
|
|
200
215
|
*/
|
|
201
216
|
function collectAllSessionIds(sessionDirs?: string[]): Set<string> {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
const dirs = sessionDirs ?? findSessionDirs();
|
|
218
|
+
const allIds = new Set<string>();
|
|
219
|
+
for (const dir of dirs) {
|
|
220
|
+
const ids = scanSessionDir(dir);
|
|
221
|
+
for (const id of ids) allIds.add(id);
|
|
222
|
+
}
|
|
223
|
+
return allIds;
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
// ── Cross-reference: find orphaned files ────────────────────────────────────
|
|
@@ -219,34 +234,37 @@ function collectAllSessionIds(sessionDirs?: string[]): Set<string> {
|
|
|
219
234
|
* Returns the full report with all files classified.
|
|
220
235
|
*/
|
|
221
236
|
function crossReference(
|
|
222
|
-
|
|
223
|
-
|
|
237
|
+
pending: PendingFile[],
|
|
238
|
+
sessionIds: Set<string>,
|
|
224
239
|
): CleanupReport {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
const orphaned: PendingFile[] = [];
|
|
241
|
+
const active: PendingFile[] = [];
|
|
242
|
+
|
|
243
|
+
for (const pf of pending) {
|
|
244
|
+
if (sessionIds.has(pf.sessionId)) {
|
|
245
|
+
active.push(pf);
|
|
246
|
+
} else {
|
|
247
|
+
orphaned.push(pf);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { all: pending, orphaned, active };
|
|
237
252
|
}
|
|
238
253
|
|
|
239
254
|
/**
|
|
240
255
|
* Full pipeline: scan pending files, collect session IDs, cross-reference.
|
|
241
256
|
* Returns the cleanup report.
|
|
242
257
|
*/
|
|
243
|
-
export function analyzeOrphaned(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
export function analyzeOrphaned(
|
|
259
|
+
agentDir?: string,
|
|
260
|
+
sessionDirs?: string[],
|
|
261
|
+
): CleanupReport {
|
|
262
|
+
const pending = scanPendingFiles(agentDir);
|
|
263
|
+
if (pending.length === 0) {
|
|
264
|
+
return { all: [], orphaned: [], active: [] };
|
|
265
|
+
}
|
|
266
|
+
const sessionIds = collectAllSessionIds(sessionDirs);
|
|
267
|
+
return crossReference(pending, sessionIds);
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
// ── Deletion ────────────────────────────────────────────────────────────────
|
|
@@ -259,25 +277,25 @@ const SAFE_SESSION_ID_RE = /^[a-zA-Z0-9][-a-zA-Z0-9]*$/;
|
|
|
259
277
|
* pi-blackhole directory. Must be called before any unlink.
|
|
260
278
|
*/
|
|
261
279
|
function validateDeletionPaths(
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
sessionId: string,
|
|
281
|
+
pendingDir: string,
|
|
264
282
|
): { ok: true; pendingPath: string; stalePath: string } | { ok: false } {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
283
|
+
// sessionId must be non-empty and contain only safe filesystem characters
|
|
284
|
+
if (!sessionId || typeof sessionId !== "string") return { ok: false };
|
|
285
|
+
if (!SAFE_SESSION_ID_RE.test(sessionId)) return { ok: false };
|
|
268
286
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
287
|
+
const resolvedDir = resolve(pendingDir);
|
|
288
|
+
const pendingPath = join(resolvedDir, `${sessionId}${PENDING_SUFFIX}`);
|
|
289
|
+
const stalePath = join(resolvedDir, `${sessionId}${STALE_SUFFIX}`);
|
|
272
290
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
// Resolve to absolute and verify containment within pi-blackhole/
|
|
292
|
+
const resolvedPending = resolve(pendingPath);
|
|
293
|
+
const resolvedStale = resolve(stalePath);
|
|
276
294
|
|
|
277
|
-
|
|
278
|
-
|
|
295
|
+
if (!resolvedPending.startsWith(resolvedDir + sep)) return { ok: false };
|
|
296
|
+
if (!resolvedStale.startsWith(resolvedDir + sep)) return { ok: false };
|
|
279
297
|
|
|
280
|
-
|
|
298
|
+
return { ok: true, pendingPath: resolvedPending, stalePath: resolvedStale };
|
|
281
299
|
}
|
|
282
300
|
|
|
283
301
|
/**
|
|
@@ -289,34 +307,37 @@ function validateDeletionPaths(
|
|
|
289
307
|
* Returns true if at least one file was deleted, false if no files existed
|
|
290
308
|
* or if the paths failed containment validation.
|
|
291
309
|
*/
|
|
292
|
-
export function deletePendingFiles(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
310
|
+
export function deletePendingFiles(
|
|
311
|
+
sessionId: string,
|
|
312
|
+
agentDir?: string,
|
|
313
|
+
): boolean {
|
|
314
|
+
const dir = join(agentDir ?? getAgentDir(), PENDING_DIR);
|
|
315
|
+
const valid = validateDeletionPaths(sessionId, dir);
|
|
316
|
+
if (!valid.ok) return false;
|
|
317
|
+
|
|
318
|
+
const { pendingPath, stalePath } = valid;
|
|
319
|
+
|
|
320
|
+
let deleted = false;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
if (existsSync(pendingPath)) {
|
|
324
|
+
unlinkSync(pendingPath);
|
|
325
|
+
deleted = true;
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Best-effort — file may be locked or already gone
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
if (existsSync(stalePath)) {
|
|
333
|
+
unlinkSync(stalePath);
|
|
334
|
+
deleted = true;
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
// Best-effort
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return deleted;
|
|
320
341
|
}
|
|
321
342
|
|
|
322
343
|
/**
|
|
@@ -324,44 +345,44 @@ export function deletePendingFiles(sessionId: string, agentDir?: string): boolea
|
|
|
324
345
|
* Returns the count of successfully deleted file sets.
|
|
325
346
|
*/
|
|
326
347
|
export function deleteOrphanedBatch(
|
|
327
|
-
|
|
328
|
-
|
|
348
|
+
orphaned: PendingFile[],
|
|
349
|
+
agentDir?: string,
|
|
329
350
|
): number {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
let count = 0;
|
|
352
|
+
for (const pf of orphaned) {
|
|
353
|
+
if (deletePendingFiles(pf.sessionId, agentDir)) {
|
|
354
|
+
count++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return count;
|
|
337
358
|
}
|
|
338
359
|
|
|
339
360
|
// ── Formatting helpers ──────────────────────────────────────────────────────
|
|
340
361
|
|
|
341
362
|
/** Human-readable file size. */
|
|
342
363
|
function formatSize(bytes: number): string {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
364
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
365
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
366
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
346
367
|
}
|
|
347
368
|
|
|
348
369
|
/** Human-readable age from epoch ms. */
|
|
349
370
|
function formatAge(mtimeMs: number, nowMs: number = Date.now()): string {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
371
|
+
const diffMs = nowMs - mtimeMs;
|
|
372
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
373
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
374
|
+
const minutes = Math.floor(seconds / 60);
|
|
375
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
376
|
+
const hours = Math.floor(minutes / 60);
|
|
377
|
+
if (hours < 24) return `${hours}h ago`;
|
|
378
|
+
const days = Math.floor(hours / 24);
|
|
379
|
+
if (days < 365) return `${days}d ago`;
|
|
380
|
+
const years = Math.floor(days / 365);
|
|
381
|
+
return `${years}y ago`;
|
|
361
382
|
}
|
|
362
383
|
|
|
363
384
|
/** Summary line for a pending file. */
|
|
364
385
|
export function describeFile(pf: PendingFile, nowMs?: number): string {
|
|
365
|
-
|
|
366
|
-
|
|
386
|
+
const label = pf.isStale ? "stale" : "pending";
|
|
387
|
+
return `${pf.sessionId.slice(0, 8)}… ${label} ${formatSize(pf.sizeBytes)} ${formatAge(pf.mtimeMs, nowMs)}`;
|
|
367
388
|
}
|