taskplane 0.16.0 → 0.17.0
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/dashboard/server.cjs
CHANGED
|
@@ -295,8 +295,18 @@ function tailJsonlFile(filePath) {
|
|
|
295
295
|
return []; // No new data
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
//
|
|
299
|
-
|
|
298
|
+
// Cap read size per tick to avoid ERR_STRING_TOO_LONG on large files.
|
|
299
|
+
// If there's more data remaining, the next SSE tick will pick up the rest.
|
|
300
|
+
const MAX_TAIL_BYTES = 10 * 1024 * 1024; // 10 MB per tick
|
|
301
|
+
|
|
302
|
+
// Skip-to-tail on fresh dashboard start with large files.
|
|
303
|
+
// The partial-line handling below already discards the first partial line.
|
|
304
|
+
if (tailState.offset === 0 && fileSize > MAX_TAIL_BYTES) {
|
|
305
|
+
tailState.offset = fileSize - MAX_TAIL_BYTES;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Read new bytes from offset, capped to MAX_TAIL_BYTES
|
|
309
|
+
const bytesToRead = Math.min(fileSize - tailState.offset, MAX_TAIL_BYTES);
|
|
300
310
|
const buf = Buffer.alloc(bytesToRead);
|
|
301
311
|
let fd;
|
|
302
312
|
try {
|
|
@@ -311,7 +321,7 @@ function tailJsonlFile(filePath) {
|
|
|
311
321
|
return []; // Read error — try again next tick
|
|
312
322
|
}
|
|
313
323
|
fs.closeSync(fd);
|
|
314
|
-
tailState.offset
|
|
324
|
+
tailState.offset += bytesToRead;
|
|
315
325
|
|
|
316
326
|
// Split into lines, preserving partial trailing line
|
|
317
327
|
const chunk = tailState.partial + buf.toString("utf-8");
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact cleanup and log rotation for orchestrator runtime files.
|
|
3
|
+
*
|
|
4
|
+
* Three cleanup layers prevent unbounded disk growth:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Post-Integrate Cleanup** — Deletes batch-specific telemetry and merge
|
|
7
|
+
* result files after successful /orch-integrate. Scoped by batchId.
|
|
8
|
+
*
|
|
9
|
+
* 2. **Age-Based Preflight Sweep** — On /orch start, removes telemetry and
|
|
10
|
+
* merge artifacts older than 7 days. Catches files missed by Layer 1
|
|
11
|
+
* (e.g., aborted batches, manual branch deletions).
|
|
12
|
+
*
|
|
13
|
+
* 3. **Size-Capped Log Rotation** — Rotates append-only supervisor logs
|
|
14
|
+
* (events.jsonl, actions.jsonl) at a 5MB threshold during preflight.
|
|
15
|
+
* Keeps one .old generation.
|
|
16
|
+
*
|
|
17
|
+
* All cleanup is **non-fatal** — failures warn but never block execution.
|
|
18
|
+
*
|
|
19
|
+
* @module orch/cleanup
|
|
20
|
+
* @since TP-065
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readdirSync, statSync, unlinkSync, renameSync, mkdirSync } from "fs";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
|
|
25
|
+
// ── Layer 1: Post-Integrate Cleanup ─────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Result of post-integrate artifact cleanup.
|
|
29
|
+
*/
|
|
30
|
+
export interface PostIntegrateCleanupResult {
|
|
31
|
+
/** Number of telemetry files deleted */
|
|
32
|
+
telemetryFilesDeleted: number;
|
|
33
|
+
/** Number of merge result/request files deleted */
|
|
34
|
+
mergeFilesDeleted: number;
|
|
35
|
+
/** Number of lane prompt files deleted */
|
|
36
|
+
promptFilesDeleted: number;
|
|
37
|
+
/** Warnings from non-fatal cleanup failures */
|
|
38
|
+
warnings: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Clean up batch-specific telemetry and merge result files after integrate.
|
|
43
|
+
*
|
|
44
|
+
* Targets files whose names contain the batchId:
|
|
45
|
+
* - `.pi/telemetry/*-{batchId}-*.jsonl` — worker/merger sidecar files
|
|
46
|
+
* - `.pi/telemetry/*-{batchId}-*-exit.json` — exit summaries
|
|
47
|
+
* - `.pi/telemetry/lane-prompt-*.txt` — temporary prompt files (all, not scoped)
|
|
48
|
+
* - `.pi/merge-result-*-{batchId}.json` — merge result files
|
|
49
|
+
* - `.pi/merge-request-*-{batchId}.txt` — merge request files
|
|
50
|
+
*
|
|
51
|
+
* @param stateRoot - Root directory containing .pi/ (workspace root or repo root)
|
|
52
|
+
* @param batchId - Batch ID to scope deletion
|
|
53
|
+
* @returns Cleanup result with counts and warnings
|
|
54
|
+
*/
|
|
55
|
+
export function cleanupPostIntegrate(stateRoot: string, batchId: string): PostIntegrateCleanupResult {
|
|
56
|
+
const result: PostIntegrateCleanupResult = {
|
|
57
|
+
telemetryFilesDeleted: 0,
|
|
58
|
+
mergeFilesDeleted: 0,
|
|
59
|
+
promptFilesDeleted: 0,
|
|
60
|
+
warnings: [],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (!batchId) {
|
|
64
|
+
result.warnings.push("No batchId provided — skipping post-integrate cleanup");
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Telemetry files (.pi/telemetry/) ─────────────────────────
|
|
69
|
+
const telemetryDir = join(stateRoot, ".pi", "telemetry");
|
|
70
|
+
if (existsSync(telemetryDir)) {
|
|
71
|
+
try {
|
|
72
|
+
const entries = readdirSync(telemetryDir);
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
// Delete batch-scoped sidecar/exit files containing the batchId
|
|
75
|
+
if (entry.includes(batchId) && (entry.endsWith(".jsonl") || entry.endsWith("-exit.json"))) {
|
|
76
|
+
try {
|
|
77
|
+
unlinkSync(join(telemetryDir, entry));
|
|
78
|
+
result.telemetryFilesDeleted++;
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
result.warnings.push(`Failed to delete telemetry file ${entry}: ${(err as Error).message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Delete all lane-prompt-*.txt files (not batch-scoped — they're
|
|
84
|
+
// temporary and should be cleaned up with any batch)
|
|
85
|
+
if (entry.startsWith("lane-prompt-") && entry.endsWith(".txt")) {
|
|
86
|
+
try {
|
|
87
|
+
unlinkSync(join(telemetryDir, entry));
|
|
88
|
+
result.promptFilesDeleted++;
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
result.warnings.push(`Failed to delete prompt file ${entry}: ${(err as Error).message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (err: unknown) {
|
|
95
|
+
result.warnings.push(`Failed to read telemetry directory: ${(err as Error).message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Merge result/request files (.pi/) ────────────────────────
|
|
100
|
+
const piDir = join(stateRoot, ".pi");
|
|
101
|
+
if (existsSync(piDir)) {
|
|
102
|
+
try {
|
|
103
|
+
const entries = readdirSync(piDir);
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.includes(batchId) && (
|
|
106
|
+
(entry.startsWith("merge-result-") && entry.endsWith(".json")) ||
|
|
107
|
+
(entry.startsWith("merge-request-") && entry.endsWith(".txt"))
|
|
108
|
+
)) {
|
|
109
|
+
try {
|
|
110
|
+
unlinkSync(join(piDir, entry));
|
|
111
|
+
result.mergeFilesDeleted++;
|
|
112
|
+
} catch (err: unknown) {
|
|
113
|
+
result.warnings.push(`Failed to delete merge file ${entry}: ${(err as Error).message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err: unknown) {
|
|
118
|
+
result.warnings.push(`Failed to read .pi directory: ${(err as Error).message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format post-integrate cleanup result for user-facing notification.
|
|
127
|
+
*/
|
|
128
|
+
export function formatPostIntegrateCleanup(result: PostIntegrateCleanupResult): string {
|
|
129
|
+
const parts: string[] = [];
|
|
130
|
+
const totalDeleted = result.telemetryFilesDeleted + result.mergeFilesDeleted + result.promptFilesDeleted;
|
|
131
|
+
|
|
132
|
+
if (totalDeleted > 0) {
|
|
133
|
+
const segments: string[] = [];
|
|
134
|
+
if (result.telemetryFilesDeleted > 0) segments.push(`${result.telemetryFilesDeleted} telemetry`);
|
|
135
|
+
if (result.mergeFilesDeleted > 0) segments.push(`${result.mergeFilesDeleted} merge`);
|
|
136
|
+
if (result.promptFilesDeleted > 0) segments.push(`${result.promptFilesDeleted} prompt`);
|
|
137
|
+
parts.push(`🧹 Cleaned up ${totalDeleted} artifact file(s): ${segments.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const warning of result.warnings) {
|
|
141
|
+
parts.push(` ⚠️ ${warning}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return parts.join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Layer 2: Age-Based Preflight Sweep ──────────────────────────────
|
|
148
|
+
|
|
149
|
+
/** Default max age for stale artifacts (7 days in milliseconds). */
|
|
150
|
+
export const STALE_ARTIFACT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Result of a preflight age-based sweep.
|
|
154
|
+
*/
|
|
155
|
+
export interface PreflightSweepResult {
|
|
156
|
+
/** Number of stale files deleted */
|
|
157
|
+
staleFilesDeleted: number;
|
|
158
|
+
/** Whether the sweep was skipped (e.g., active batch) */
|
|
159
|
+
skipped: boolean;
|
|
160
|
+
/** Reason for skipping (if skipped) */
|
|
161
|
+
skipReason?: string;
|
|
162
|
+
/** Warnings from non-fatal cleanup failures */
|
|
163
|
+
warnings: string[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Dependencies injected into sweepStaleArtifacts for testability.
|
|
168
|
+
*/
|
|
169
|
+
export interface SweepDeps {
|
|
170
|
+
/** Check if a batch is currently active (phase is not terminal). */
|
|
171
|
+
isBatchActive: () => boolean;
|
|
172
|
+
/** Get the current timestamp (for deterministic testing). */
|
|
173
|
+
now: () => number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Sweep stale artifacts older than maxAgeMs during preflight.
|
|
178
|
+
*
|
|
179
|
+
* Targets:
|
|
180
|
+
* - `.pi/telemetry/*.jsonl` — sidecar files
|
|
181
|
+
* - `.pi/telemetry/*-exit.json` — exit summaries
|
|
182
|
+
* - `.pi/telemetry/lane-prompt-*.txt` — temporary prompt files
|
|
183
|
+
* - `.pi/merge-result-*.json` — merge result files
|
|
184
|
+
* - `.pi/merge-request-*.txt` — merge request files
|
|
185
|
+
*
|
|
186
|
+
* Uses file mtime for age detection. Skips files modified within maxAgeMs.
|
|
187
|
+
* If a batch is currently active (executing/merging), skips ALL cleanup.
|
|
188
|
+
*
|
|
189
|
+
* @param stateRoot - Root directory containing .pi/
|
|
190
|
+
* @param deps - Injectable dependencies for testability
|
|
191
|
+
* @param maxAgeMs - Maximum file age in milliseconds (default: 7 days)
|
|
192
|
+
* @returns Sweep result with count and warnings
|
|
193
|
+
*/
|
|
194
|
+
export function sweepStaleArtifacts(
|
|
195
|
+
stateRoot: string,
|
|
196
|
+
deps: SweepDeps,
|
|
197
|
+
maxAgeMs: number = STALE_ARTIFACT_MAX_AGE_MS,
|
|
198
|
+
): PreflightSweepResult {
|
|
199
|
+
const result: PreflightSweepResult = {
|
|
200
|
+
staleFilesDeleted: 0,
|
|
201
|
+
skipped: false,
|
|
202
|
+
warnings: [],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Guard: skip if batch is actively executing
|
|
206
|
+
try {
|
|
207
|
+
if (deps.isBatchActive()) {
|
|
208
|
+
result.skipped = true;
|
|
209
|
+
result.skipReason = "Active batch detected — skipping stale artifact sweep";
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// If we can't determine batch state, proceed cautiously
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const now = deps.now();
|
|
217
|
+
const cutoff = now - maxAgeMs;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Delete files older than cutoff from a directory, matching a filter.
|
|
221
|
+
*/
|
|
222
|
+
const sweepDir = (dir: string, filter: (name: string) => boolean): void => {
|
|
223
|
+
if (!existsSync(dir)) return;
|
|
224
|
+
try {
|
|
225
|
+
const entries = readdirSync(dir);
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (!filter(entry)) continue;
|
|
228
|
+
const filePath = join(dir, entry);
|
|
229
|
+
try {
|
|
230
|
+
const stat = statSync(filePath);
|
|
231
|
+
if (!stat.isFile()) continue;
|
|
232
|
+
if (stat.mtimeMs < cutoff) {
|
|
233
|
+
unlinkSync(filePath);
|
|
234
|
+
result.staleFilesDeleted++;
|
|
235
|
+
}
|
|
236
|
+
} catch (err: unknown) {
|
|
237
|
+
result.warnings.push(`Failed to process ${entry}: ${(err as Error).message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (err: unknown) {
|
|
241
|
+
result.warnings.push(`Failed to read directory ${dir}: ${(err as Error).message}`);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Sweep telemetry files
|
|
246
|
+
sweepDir(join(stateRoot, ".pi", "telemetry"), (name) =>
|
|
247
|
+
name.endsWith(".jsonl") ||
|
|
248
|
+
name.endsWith("-exit.json") ||
|
|
249
|
+
(name.startsWith("lane-prompt-") && name.endsWith(".txt")),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Sweep merge result/request files
|
|
253
|
+
sweepDir(join(stateRoot, ".pi"), (name) =>
|
|
254
|
+
(name.startsWith("merge-result-") && name.endsWith(".json")) ||
|
|
255
|
+
(name.startsWith("merge-request-") && name.endsWith(".txt")),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Format preflight sweep result for logging.
|
|
263
|
+
*/
|
|
264
|
+
export function formatPreflightSweep(result: PreflightSweepResult): string {
|
|
265
|
+
if (result.skipped) {
|
|
266
|
+
return `ℹ️ Preflight sweep skipped: ${result.skipReason}`;
|
|
267
|
+
}
|
|
268
|
+
if (result.staleFilesDeleted === 0 && result.warnings.length === 0) {
|
|
269
|
+
return ""; // Nothing to report
|
|
270
|
+
}
|
|
271
|
+
const parts: string[] = [];
|
|
272
|
+
if (result.staleFilesDeleted > 0) {
|
|
273
|
+
parts.push(`🧹 Preflight cleanup: removed ${result.staleFilesDeleted} stale artifact(s) (>7 days old)`);
|
|
274
|
+
}
|
|
275
|
+
for (const warning of result.warnings) {
|
|
276
|
+
parts.push(` ⚠️ ${warning}`);
|
|
277
|
+
}
|
|
278
|
+
return parts.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Layer 3: Size-Capped Log Rotation ───────────────────────────────
|
|
282
|
+
|
|
283
|
+
/** Default rotation threshold: 5MB. */
|
|
284
|
+
export const LOG_ROTATION_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Result of log rotation.
|
|
288
|
+
*/
|
|
289
|
+
export interface LogRotationResult {
|
|
290
|
+
/** Files that were rotated */
|
|
291
|
+
rotated: string[];
|
|
292
|
+
/** Warnings from non-fatal rotation failures */
|
|
293
|
+
warnings: string[];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Rotate supervisor append-only logs at a size threshold.
|
|
298
|
+
*
|
|
299
|
+
* Checks `events.jsonl` and `actions.jsonl` in `.pi/supervisor/`.
|
|
300
|
+
* If a file exceeds the threshold, renames it to `.old` (overwriting
|
|
301
|
+
* any existing `.old`), allowing a fresh file to be created on next write.
|
|
302
|
+
*
|
|
303
|
+
* Only call during preflight (not mid-batch).
|
|
304
|
+
*
|
|
305
|
+
* @param stateRoot - Root directory containing .pi/
|
|
306
|
+
* @param thresholdBytes - Maximum file size before rotation (default: 5MB)
|
|
307
|
+
* @returns Rotation result
|
|
308
|
+
*/
|
|
309
|
+
export function rotateSupervisorLogs(
|
|
310
|
+
stateRoot: string,
|
|
311
|
+
thresholdBytes: number = LOG_ROTATION_THRESHOLD_BYTES,
|
|
312
|
+
): LogRotationResult {
|
|
313
|
+
const result: LogRotationResult = {
|
|
314
|
+
rotated: [],
|
|
315
|
+
warnings: [],
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const supervisorDir = join(stateRoot, ".pi", "supervisor");
|
|
319
|
+
if (!existsSync(supervisorDir)) {
|
|
320
|
+
return result; // Nothing to rotate
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const filesToRotate = ["events.jsonl", "actions.jsonl"];
|
|
324
|
+
|
|
325
|
+
for (const fileName of filesToRotate) {
|
|
326
|
+
const filePath = join(supervisorDir, fileName);
|
|
327
|
+
if (!existsSync(filePath)) continue;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const stat = statSync(filePath);
|
|
331
|
+
if (!stat.isFile() || stat.size <= thresholdBytes) continue;
|
|
332
|
+
|
|
333
|
+
const oldPath = `${filePath}.old`;
|
|
334
|
+
renameSync(filePath, oldPath);
|
|
335
|
+
result.rotated.push(fileName);
|
|
336
|
+
} catch (err: unknown) {
|
|
337
|
+
result.warnings.push(`Failed to rotate ${fileName}: ${(err as Error).message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Format log rotation result for logging.
|
|
346
|
+
*/
|
|
347
|
+
export function formatLogRotation(result: LogRotationResult): string {
|
|
348
|
+
if (result.rotated.length === 0 && result.warnings.length === 0) {
|
|
349
|
+
return ""; // Nothing to report
|
|
350
|
+
}
|
|
351
|
+
const parts: string[] = [];
|
|
352
|
+
if (result.rotated.length > 0) {
|
|
353
|
+
parts.push(`🔄 Rotated ${result.rotated.length} supervisor log(s): ${result.rotated.join(", ")}`);
|
|
354
|
+
}
|
|
355
|
+
for (const warning of result.warnings) {
|
|
356
|
+
parts.push(` ⚠️ ${warning}`);
|
|
357
|
+
}
|
|
358
|
+
return parts.join("\n");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Combined Preflight Cleanup ──────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Combined result of preflight cleanup (Layer 2 + Layer 3).
|
|
365
|
+
*/
|
|
366
|
+
export interface PreflightCleanupResult {
|
|
367
|
+
sweep: PreflightSweepResult;
|
|
368
|
+
rotation: LogRotationResult;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Run all preflight cleanup operations (Layer 2 + Layer 3).
|
|
373
|
+
*
|
|
374
|
+
* Called from the engine's preflight phase before batch starts.
|
|
375
|
+
* Always non-fatal.
|
|
376
|
+
*
|
|
377
|
+
* @param stateRoot - Root directory containing .pi/
|
|
378
|
+
* @param deps - Sweep dependencies (active batch check)
|
|
379
|
+
* @returns Combined cleanup result
|
|
380
|
+
*/
|
|
381
|
+
export function runPreflightCleanup(
|
|
382
|
+
stateRoot: string,
|
|
383
|
+
deps: SweepDeps,
|
|
384
|
+
): PreflightCleanupResult {
|
|
385
|
+
const sweep = sweepStaleArtifacts(stateRoot, deps);
|
|
386
|
+
const rotation = rotateSupervisorLogs(stateRoot);
|
|
387
|
+
return { sweep, rotation };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Format combined preflight cleanup result for user notification.
|
|
392
|
+
*
|
|
393
|
+
* Returns an empty string if nothing happened (no files cleaned/rotated).
|
|
394
|
+
*/
|
|
395
|
+
export function formatPreflightCleanup(result: PreflightCleanupResult): string {
|
|
396
|
+
const parts: string[] = [];
|
|
397
|
+
|
|
398
|
+
// Layer 2: age-based sweep
|
|
399
|
+
if (!result.sweep.skipped && result.sweep.staleFilesDeleted > 0) {
|
|
400
|
+
parts.push(`removed ${result.sweep.staleFilesDeleted} stale artifact(s) (>7 days old)`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Layer 3: log rotation
|
|
404
|
+
if (result.rotation.rotated.length > 0) {
|
|
405
|
+
parts.push(`rotated ${result.rotation.rotated.join(", ")} (>5 MB)`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Collect warnings from both layers
|
|
409
|
+
const warnings = [...result.sweep.warnings, ...result.rotation.warnings];
|
|
410
|
+
if (warnings.length > 0) {
|
|
411
|
+
parts.push(`⚠️ ${warnings.length} cleanup warning(s)`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (parts.length === 0) return "";
|
|
415
|
+
return `🧹 Preflight cleanup: ${parts.join("; ")}`;
|
|
416
|
+
}
|
|
@@ -16,12 +16,13 @@ import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolic
|
|
|
16
16
|
import type { CleanupGateRepoFailure } from "./messages.ts";
|
|
17
17
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
18
18
|
import { resolveOperatorId } from "./naming.ts";
|
|
19
|
-
import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
19
|
+
import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, loadBatchState, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
20
20
|
import { listOrchSessions } from "./sessions.ts";
|
|
21
21
|
import { buildEngineEventBase, defaultResilienceState, FATAL_DISCOVERY_CODES, generateBatchId, TIER0_RETRYABLE_CLASSIFICATIONS, TIER0_RETRY_BUDGETS, tier0ScopeKey, tier0WaveScopeKey } from "./types.ts";
|
|
22
22
|
import type { AllocatedLane, AllocatedTask, BatchHistorySummary, BatchTaskSummary, BatchWaveSummary, DiscoveryResult, EngineEventCallback, EscalationContext, LaneExecutionResult, LaneTaskOutcome, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, TaskRunnerConfig, Tier0EscalationPattern, Tier0RecoveryPattern, TokenCounts, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
23
23
|
import { buildDependencyGraph, computeWaves, resolveBaseBranch, resolveRepoRoot, validateGraph } from "./waves.ts";
|
|
24
24
|
import { deleteBranchBestEffort, forceCleanupWorktree, formatPreflightResults, listWorktrees, preserveFailedLaneProgress, removeAllWorktrees, removeWorktree, runPreflight, safeResetWorktree, sleepSync } from "./worktree.ts";
|
|
25
|
+
import { runPreflightCleanup, formatPreflightCleanup } from "./cleanup.ts";
|
|
25
26
|
|
|
26
27
|
// ── Tier 0: Automatic Recovery Helpers (TP-039) ─────────────────────
|
|
27
28
|
|
|
@@ -853,6 +854,39 @@ export async function executeOrchBatch(
|
|
|
853
854
|
return;
|
|
854
855
|
}
|
|
855
856
|
|
|
857
|
+
// ── TP-065: Preflight artifact cleanup (Layer 2 + Layer 3) ───
|
|
858
|
+
// Sweep stale artifacts and rotate oversized logs before batch starts.
|
|
859
|
+
// Always non-fatal — failures warn but never block batch execution.
|
|
860
|
+
try {
|
|
861
|
+
// Layer 2: Age-based sweep of stale telemetry/merge artifacts (>7 days)
|
|
862
|
+
const sweepResult = sweepStaleArtifacts(stateRoot, {
|
|
863
|
+
isBatchActive: () => {
|
|
864
|
+
// Check persisted state — a prior batch may still be active
|
|
865
|
+
try {
|
|
866
|
+
const state = loadBatchState(stateRoot);
|
|
867
|
+
if (state && state.phase !== "completed" && state.phase !== "failed" && state.phase !== "stopped") {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
} catch { /* state unreadable — safe to sweep */ }
|
|
871
|
+
return false;
|
|
872
|
+
},
|
|
873
|
+
now: () => Date.now(),
|
|
874
|
+
});
|
|
875
|
+
const sweepMsg = formatPreflightSweep(sweepResult);
|
|
876
|
+
if (sweepMsg) {
|
|
877
|
+
onNotify(sweepMsg, "info");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Layer 3: Size-capped rotation of supervisor append-only logs
|
|
881
|
+
const rotationResult = rotateSupervisorLogs(stateRoot);
|
|
882
|
+
const rotationMsg = formatLogRotation(rotationResult);
|
|
883
|
+
if (rotationMsg) {
|
|
884
|
+
onNotify(rotationMsg, "info");
|
|
885
|
+
}
|
|
886
|
+
} catch {
|
|
887
|
+
// Non-fatal — never block batch start for cleanup errors
|
|
888
|
+
}
|
|
889
|
+
|
|
856
890
|
// Discovery — task area paths in task-runner.yaml are workspace-relative.
|
|
857
891
|
// In repo mode workspaceRoot === repoRoot, so this is always correct.
|
|
858
892
|
const discoveryRoot = workspaceRoot ?? cwd;
|
|
@@ -47,6 +47,7 @@ import { buildExecutionContext } from "./workspace.ts";
|
|
|
47
47
|
import { openSettingsTui } from "./settings-tui.ts";
|
|
48
48
|
import { loadProjectConfig } from "./config-loader.ts";
|
|
49
49
|
import { runMigrations } from "./migrations.ts";
|
|
50
|
+
import { cleanupPostIntegrate, formatPostIntegrateCleanup, sweepStaleArtifacts, formatPreflightSweep, rotateSupervisorLogs, formatLogRotation } from "./cleanup.ts";
|
|
50
51
|
import {
|
|
51
52
|
activateSupervisor,
|
|
52
53
|
deactivateSupervisor,
|
|
@@ -977,6 +978,12 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string): Integ
|
|
|
977
978
|
deleteStaleBranches(repoRoot, opId, context.batchId);
|
|
978
979
|
dropBatchAutostash(repoRoot, context.batchId);
|
|
979
980
|
} catch { /* best effort — don't fail integration for cleanup errors */ }
|
|
981
|
+
|
|
982
|
+
// TP-065: Post-integrate artifact cleanup (Layer 1).
|
|
983
|
+
// Also runs on the supervisor auto-integration path.
|
|
984
|
+
try {
|
|
985
|
+
cleanupPostIntegrate(repoRoot, context.batchId);
|
|
986
|
+
} catch { /* best effort — don't fail integration for cleanup errors */ }
|
|
980
987
|
}
|
|
981
988
|
|
|
982
989
|
return result;
|
|
@@ -2282,6 +2289,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
2282
2289
|
|
|
2283
2290
|
try { deleteBatchState(repoRoot); } catch { /* best effort */ }
|
|
2284
2291
|
|
|
2292
|
+
// ── TP-065: Post-integrate artifact cleanup (Layer 1) ────
|
|
2293
|
+
// Delete batch-specific telemetry and merge result files.
|
|
2294
|
+
// Non-fatal — failures warn but don't block integration.
|
|
2295
|
+
if (batchId) {
|
|
2296
|
+
try {
|
|
2297
|
+
const artifactCleanup = cleanupPostIntegrate(repoRoot, batchId);
|
|
2298
|
+
const totalCleaned = artifactCleanup.telemetryFilesDeleted + artifactCleanup.mergeFilesDeleted + artifactCleanup.promptFilesDeleted;
|
|
2299
|
+
if (totalCleaned > 0) {
|
|
2300
|
+
outputLines.push(
|
|
2301
|
+
`🧹 Cleaned up ${artifactCleanup.telemetryFilesDeleted} telemetry file(s), ` +
|
|
2302
|
+
`${artifactCleanup.mergeFilesDeleted} merge result(s), ` +
|
|
2303
|
+
`${artifactCleanup.promptFilesDeleted} prompt file(s) for batch ${batchId}`,
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
if (artifactCleanup.warnings.length > 0) {
|
|
2307
|
+
hasWarning = true;
|
|
2308
|
+
outputLines.push(`⚠️ Artifact cleanup warnings: ${artifactCleanup.warnings.join("; ")}`);
|
|
2309
|
+
}
|
|
2310
|
+
} catch {
|
|
2311
|
+
// Non-fatal — never block integration for cleanup failures
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2285
2315
|
const integrationSummary = wsConfig
|
|
2286
2316
|
? `✅ Integrated ${resolvedOrchBranch} across ${reposToIntegrate.length} repo(s).\n${repoMessages.join("\n")}\n${totalCommits} total commit(s) applied.`
|
|
2287
2317
|
: `${repoMessages[0] || "✅ Integrated."}\n${commitsAhead} commit(s) applied.`;
|