taskplane 0.16.0 → 0.18.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 +17 -4
- package/extensions/task-runner.ts +10 -3
- package/extensions/taskplane/cleanup.ts +416 -0
- package/extensions/taskplane/engine.ts +35 -1
- package/extensions/taskplane/extension.ts +30 -0
- package/extensions/taskplane/index.ts +1 -0
- package/package.json +1 -1
- package/templates/agents/local/task-worker.md +1 -0
- package/templates/agents/task-worker.md +37 -0
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");
|
|
@@ -452,8 +462,11 @@ function loadTelemetryData(batchState) {
|
|
|
452
462
|
? (usage.cost.total || 0)
|
|
453
463
|
: (typeof usage.cost === "number" ? usage.cost : 0);
|
|
454
464
|
}
|
|
455
|
-
|
|
465
|
+
// Include cacheRead: totalTokens from pi excludes cache reads,
|
|
466
|
+
// but cached tokens still consume context window capacity.
|
|
467
|
+
const rawTotal = usage.totalTokens
|
|
456
468
|
|| ((usage.input || 0) + (usage.output || 0));
|
|
469
|
+
const totalTokens = rawTotal + (usage.cacheRead || 0);
|
|
457
470
|
if (totalTokens > acc.latestTotalTokens) {
|
|
458
471
|
acc.latestTotalTokens = totalTokens;
|
|
459
472
|
}
|
|
@@ -1204,7 +1204,10 @@ function spawnAgent(opts: {
|
|
|
1204
1204
|
// Use totalTokens (cumulative) — works across providers.
|
|
1205
1205
|
// Anthropic reports small `input` per-turn but growing `totalTokens`.
|
|
1206
1206
|
// OpenAI reports growing `input` but also growing `totalTokens`.
|
|
1207
|
-
|
|
1207
|
+
// Include cacheRead: pi's totalTokens excludes cache reads,
|
|
1208
|
+
// but cached tokens still consume context window capacity.
|
|
1209
|
+
const rawTokens = (usage as any).totalTokens || ((usage as any).input + (usage as any).output) || 0;
|
|
1210
|
+
const tokens = rawTokens + ((usage as any).cacheRead || 0);
|
|
1208
1211
|
if (tokens > 0) {
|
|
1209
1212
|
const pct = (tokens / opts.contextWindow) * 100;
|
|
1210
1213
|
opts.onContextPct?.(pct);
|
|
@@ -1380,9 +1383,13 @@ function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): Sideca
|
|
|
1380
1383
|
? (usage.cost.total || 0)
|
|
1381
1384
|
: (typeof usage.cost === "number" ? usage.cost : 0);
|
|
1382
1385
|
}
|
|
1383
|
-
// totalTokens is cumulative (grows each turn) — use latest value
|
|
1384
|
-
|
|
1386
|
+
// totalTokens is cumulative (grows each turn) — use latest value.
|
|
1387
|
+
// Include cacheRead tokens: pi's totalTokens and the
|
|
1388
|
+
// input+output fallback both exclude cache reads, but cached
|
|
1389
|
+
// tokens still consume context window capacity.
|
|
1390
|
+
const rawTotal = usage.totalTokens
|
|
1385
1391
|
|| ((usage.input || 0) + (usage.output || 0));
|
|
1392
|
+
const totalTokens = rawTotal + (usage.cacheRead || 0);
|
|
1386
1393
|
if (totalTokens > delta.latestTotalTokens) {
|
|
1387
1394
|
delta.latestTotalTokens = totalTokens;
|
|
1388
1395
|
}
|
|
@@ -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.`;
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ name: task-worker
|
|
|
19
19
|
- Review protocol (inline reviews via review_step tool when available)
|
|
20
20
|
- Review response handling
|
|
21
21
|
- Test execution strategy (targeted tests during steps, full suite at gate)
|
|
22
|
+
- File reading strategy (grep-first for large files, context budget awareness)
|
|
22
23
|
|
|
23
24
|
Add project-specific rules below. Common examples:
|
|
24
25
|
- Preferred package manager (pnpm, yarn, bun)
|
|
@@ -265,4 +265,41 @@ checkpoints protect against regressions even when intermediate steps use targete
|
|
|
265
265
|
2. The merge agent (before merging to the orchestrator branch)
|
|
266
266
|
3. CI (before merging to main)
|
|
267
267
|
|
|
268
|
+
## File Reading Strategy (Context Budget)
|
|
269
|
+
|
|
270
|
+
Your context window is finite. Reading large files whole wastes budget and risks
|
|
271
|
+
triggering the context-pressure safety net (85% → wrap-up, 95% → kill).
|
|
272
|
+
Use targeted reads instead:
|
|
273
|
+
|
|
274
|
+
### Pattern: grep-first, read-with-offset
|
|
275
|
+
|
|
276
|
+
1. **Locate** the relevant section with `grep` or `find`:
|
|
277
|
+
```
|
|
278
|
+
grep -n "function buildPrompt" extensions/task-runner.ts
|
|
279
|
+
```
|
|
280
|
+
2. **Read** just that region with `offset` and `limit`:
|
|
281
|
+
```
|
|
282
|
+
read extensions/task-runner.ts (offset: 1773, limit: 50)
|
|
283
|
+
```
|
|
284
|
+
3. **Edit** surgically with exact `oldText → newText`
|
|
285
|
+
|
|
286
|
+
### When to read a full file
|
|
287
|
+
|
|
288
|
+
- Files under ~500 lines — read the whole thing, it's fine
|
|
289
|
+
- Config files, test files, templates — usually small enough to read fully
|
|
290
|
+
- New files you're creating — read after writing to verify
|
|
291
|
+
|
|
292
|
+
### When NOT to read a full file
|
|
293
|
+
|
|
294
|
+
- Source files over ~1000 lines — grep first, read the relevant region
|
|
295
|
+
- Generated files, lock files, large data files — almost never need full reads
|
|
296
|
+
- Files you've already read this session — re-read only the changed region
|
|
297
|
+
|
|
298
|
+
### Getting a file outline
|
|
299
|
+
|
|
300
|
+
To understand a large file's structure without reading it all:
|
|
301
|
+
```bash
|
|
302
|
+
grep -n "^function\|^export\|^class\|^interface\|^const.*=" file.ts | head -50
|
|
303
|
+
```
|
|
304
|
+
|
|
268
305
|
|