@dyyz1993/pi-coding-agent 0.74.47 → 0.74.48
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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +9 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +28 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +89 -10
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/ask-tools/index.ts +45 -0
- package/dist/extensions/auto-session-title/index.ts +2 -0
- package/dist/extensions/compaction-manager/index.ts +68 -7
- package/dist/extensions/coordinator/index.ts +39 -0
- package/dist/extensions/hooks-engine/index.ts +3 -0
- package/dist/extensions/lsp/lsp/client/smart-file-tracker.ts +302 -0
- package/dist/extensions/lsp/lsp/utils/project-scanner.ts +101 -12
- package/dist/extensions/output-guard/index.ts +39 -0
- package/dist/extensions/preview/index.ts +23 -0
- package/dist/extensions/subagent-v2/extract-parent-todos.test.ts +146 -0
- package/dist/extensions/subagent-v2/index.ts +372 -15
- package/dist/extensions/todo-ext/index.ts +55 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +6 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,8 @@ function loadConfig(): CompactionManagerConfig {
|
|
|
27
27
|
return DEFAULT_CONFIG;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
let compactMetrics = { foldCount: 0, memoryCompactCount: 0, forceCompactCount: 0, rateLimitHits: 0, serverErrors: 0 };
|
|
31
|
+
|
|
30
32
|
export default function (pi: ExtensionAPI) {
|
|
31
33
|
const config = loadConfig();
|
|
32
34
|
|
|
@@ -70,11 +72,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
70
72
|
pi.foldEntry(entry.id, summary, tokens);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
compactMetrics.foldCount++;
|
|
73
76
|
ctx.ui.notify(`Context fold: folded ${foldable.length} old message(s)`, "info");
|
|
77
|
+
pi.appendEntry("compaction_fold", {
|
|
78
|
+
count: foldable.length,
|
|
79
|
+
totalFolds: compactMetrics.foldCount,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
});
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
if (config.sessionMemory.enabled) {
|
|
78
86
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
79
87
|
const { preparation, signal } = event;
|
|
80
88
|
|
|
@@ -84,10 +92,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
84
92
|
const result = buildMemorySummary(memoryFiles, preparation, config.sessionMemory.minContentLength);
|
|
85
93
|
if (!result) return;
|
|
86
94
|
|
|
95
|
+
compactMetrics.memoryCompactCount++;
|
|
87
96
|
ctx.ui.notify(
|
|
88
97
|
`Session Memory Compact: using ${memoryFiles.size} memory files instead of LLM summary`,
|
|
89
98
|
"info",
|
|
90
99
|
);
|
|
100
|
+
pi.appendEntry("compaction_session_memory", {
|
|
101
|
+
memoryFiles: memoryFiles.size,
|
|
102
|
+
totalMemory: compactMetrics.memoryCompactCount,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
});
|
|
91
105
|
|
|
92
106
|
return { compaction: result };
|
|
93
107
|
});
|
|
@@ -98,9 +112,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
98
112
|
|
|
99
113
|
pi.on("after_provider_response", (event, ctx) => {
|
|
100
114
|
if (event.status === 429) {
|
|
115
|
+
compactMetrics.rateLimitHits++;
|
|
101
116
|
ctx.ui.notify("Rate limited — API is throttling requests", "warning");
|
|
117
|
+
pi.sendUserMessage(
|
|
118
|
+
"[System: API Rate Limited] Requests are being throttled. The session will retry automatically. Consider reducing tool call frequency or waiting.",
|
|
119
|
+
{ deliverAs: "followUp" },
|
|
120
|
+
);
|
|
121
|
+
pi.appendEntry("compaction_rate_limit", {
|
|
122
|
+
total: compactMetrics.rateLimitHits,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
});
|
|
102
125
|
} else if (event.status >= 500) {
|
|
126
|
+
compactMetrics.serverErrors++;
|
|
103
127
|
ctx.ui.notify(`API server error (${event.status}) — will retry automatically`, "warning");
|
|
128
|
+
pi.sendUserMessage(
|
|
129
|
+
`[System: API Error] Server error (${event.status}) encountered. The session will retry automatically.`,
|
|
130
|
+
{ deliverAs: "followUp" },
|
|
131
|
+
);
|
|
132
|
+
pi.appendEntry("compaction_server_error", {
|
|
133
|
+
status: event.status,
|
|
134
|
+
total: compactMetrics.serverErrors,
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
});
|
|
104
137
|
}
|
|
105
138
|
});
|
|
106
139
|
|
|
@@ -111,18 +144,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
111
144
|
const { tokens, contextWindow, percent } = usage;
|
|
112
145
|
|
|
113
146
|
if (shouldForceCompact(tokens, contextWindow, config.reactive.forceCompactPercent) && !warnedThisTurn) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
const msg = `Context critical: ${percent!.toFixed(0)}% (${tokens!.toLocaleString()} / ${contextWindow.toLocaleString()} tokens). Automatic compaction imminent.`;
|
|
148
|
+
ctx.ui.notify(msg, "warning");
|
|
149
|
+
pi.sendUserMessage(
|
|
150
|
+
`[System: Context Warning — Action Required]\n`
|
|
151
|
+
+ `Context window is at ${percent!.toFixed(0)}% — critical. Automatic compaction will occur soon.\n\n`
|
|
152
|
+
+ `Compaction is lossy: after compression, the conversation context may lose detail, making current results unreliable.\n\n`
|
|
153
|
+
+ `Recommended actions:\n`
|
|
154
|
+
+ `1. Return your current findings and conclusions to the parent session now (before compaction).\n`
|
|
155
|
+
+ `2. If the task is complex, break it down: return your direction/framework to the parent,\n`
|
|
156
|
+
+ ` so the parent can re-dispatch more targeted sub-tasks with fresh context.\n`
|
|
157
|
+
+ `3. Use available communication tools to send results back.`,
|
|
158
|
+
{ deliverAs: "followUp" },
|
|
117
159
|
);
|
|
118
160
|
warnedThisTurn = true;
|
|
119
161
|
return;
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
if (shouldWarn(tokens, contextWindow, config.reactive.warnPercent) && !warnedThisTurn) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
const msg = `Context high: ${percent!.toFixed(0)}% (${tokens!.toLocaleString()} / ${contextWindow.toLocaleString()} tokens)`;
|
|
166
|
+
ctx.ui.notify(msg, "info");
|
|
167
|
+
pi.sendUserMessage(
|
|
168
|
+
`[System: Context Warning — Proactive]\n`
|
|
169
|
+
+ `Context window at ${percent!.toFixed(0)}% — approaching the compaction threshold.\n\n`
|
|
170
|
+
+ `Before automatic compaction reduces context fidelity:\n`
|
|
171
|
+
+ `- Consider completing the current task and returning results now.\n`
|
|
172
|
+
+ `- If the task is still large, return your current findings and direction,\n`
|
|
173
|
+
+ ` so the parent can re-dispatch with a more focused scope.\n`
|
|
174
|
+
+ `- Lossy compaction will make current details unreliable after compression.`,
|
|
175
|
+
{ deliverAs: "followUp" },
|
|
126
176
|
);
|
|
127
177
|
warnedThisTurn = true;
|
|
128
178
|
}
|
|
@@ -139,10 +189,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
139
189
|
ctx.compact({
|
|
140
190
|
customInstructions: instructions,
|
|
141
191
|
onComplete: (result) => {
|
|
192
|
+
compactMetrics.forceCompactCount++;
|
|
142
193
|
ctx.ui.notify(`Compaction done: ${result.tokensBefore.toLocaleString()} tokens compressed`, "info");
|
|
194
|
+
pi.appendEntry("compaction_force", {
|
|
195
|
+
tokensBefore: result.tokensBefore,
|
|
196
|
+
total: compactMetrics.forceCompactCount,
|
|
197
|
+
timestamp: Date.now(),
|
|
198
|
+
});
|
|
143
199
|
},
|
|
144
200
|
onError: (error) => {
|
|
145
201
|
ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
|
|
202
|
+
pi.appendEntry("compaction_failed", {
|
|
203
|
+
error: error.message,
|
|
204
|
+
total: compactMetrics.forceCompactCount,
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
});
|
|
146
207
|
},
|
|
147
208
|
});
|
|
148
209
|
},
|
|
@@ -161,12 +161,22 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
161
161
|
const result = await serverProxy.delegate(params.task, projectPath);
|
|
162
162
|
|
|
163
163
|
if (!result.sessionId) {
|
|
164
|
+
console.debug("[coordinator] delegate failed: no sessionId returned");
|
|
165
|
+
pi.appendEntry("coordinator_delegate_failed", { task: params.task, projectPath });
|
|
164
166
|
return {
|
|
165
167
|
content: [{ type: "text" as const, text: `Failed to delegate task: no sessionId returned.` }],
|
|
166
168
|
details: { error: "no sessionId" },
|
|
167
169
|
};
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
pi.appendEntry("coordinator_delegate", {
|
|
173
|
+
sessionId: result.sessionId,
|
|
174
|
+
status: result.status,
|
|
175
|
+
task: params.task,
|
|
176
|
+
title: params.title,
|
|
177
|
+
projectPath,
|
|
178
|
+
dispatchedBy: sid,
|
|
179
|
+
});
|
|
170
180
|
return {
|
|
171
181
|
content: [{ type: "text" as const, text: `Delegated task to session ${result.sessionId} (status: ${result.status}, cwd: ${projectPath}). Use session_delegate_send to communicate.` }],
|
|
172
182
|
details: { ...result, dispatchedBy: sid, projectPath },
|
|
@@ -190,12 +200,14 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
190
200
|
const result = await serverProxy.delegate_send(sid, params.targetSessionId, params.message);
|
|
191
201
|
|
|
192
202
|
if (!result.delivered) {
|
|
203
|
+
pi.appendEntry("coordinator_send_failed", { fromSessionId: sid, toSessionId: params.targetSessionId });
|
|
193
204
|
return {
|
|
194
205
|
content: [{ type: "text" as const, text: `Could not deliver message to ${params.targetSessionId}: session not found (the session file may have been deleted from disk)` }],
|
|
195
206
|
details: { delivered: false, targetSessionId: params.targetSessionId },
|
|
196
207
|
};
|
|
197
208
|
}
|
|
198
209
|
|
|
210
|
+
pi.appendEntry("coordinator_send", { fromSessionId: sid, toSessionId: params.targetSessionId, status: result.targetStatus });
|
|
199
211
|
return {
|
|
200
212
|
content: [{ type: "text" as const, text: `Message delivered to ${params.targetSessionId} (status: ${result.targetStatus})` }],
|
|
201
213
|
details: result,
|
|
@@ -253,6 +265,15 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
253
265
|
const sid = currentSessionId || ctx.sessionManager.getSessionId();
|
|
254
266
|
const projectPath = params.projectPath || ctx.cwd;
|
|
255
267
|
const result = await serverProxy.delegate_fork(params.sessionId, params.task, params.title, projectPath);
|
|
268
|
+
pi.appendEntry("coordinator_fork", {
|
|
269
|
+
sessionId: result.sessionId,
|
|
270
|
+
forkedFrom: params.sessionId,
|
|
271
|
+
status: result.status,
|
|
272
|
+
task: params.task,
|
|
273
|
+
title: params.title,
|
|
274
|
+
projectPath,
|
|
275
|
+
dispatchedBy: sid,
|
|
276
|
+
});
|
|
256
277
|
return {
|
|
257
278
|
content: [{ type: "text" as const, text: `Forked session ${params.sessionId} → ${result.sessionId} (status: ${result.status}, cwd: ${projectPath}). Task: ${params.task}` }],
|
|
258
279
|
details: { ...result, forkedFrom: params.sessionId, dispatchedBy: sid, projectPath },
|
|
@@ -260,6 +281,21 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
260
281
|
},
|
|
261
282
|
});
|
|
262
283
|
|
|
284
|
+
pi.registerTool({
|
|
285
|
+
name: "session_delegate_stop",
|
|
286
|
+
label: "Session Delegate Stop",
|
|
287
|
+
description: "Stop a delegated task session.",
|
|
288
|
+
parameters: DelegateStopParams,
|
|
289
|
+
async execute(toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
290
|
+
const ok = await serverProxy.delegate_stop(params.sessionId);
|
|
291
|
+
pi.appendEntry("coordinator_stop", { sessionId: params.sessionId, ok });
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: "text" as const, text: ok ? `Session ${params.sessionId} stopped.` : `Session ${params.sessionId} not found or already stopped.` }],
|
|
294
|
+
details: { ok },
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
263
299
|
pi.registerTool({
|
|
264
300
|
name: "session_delegate_remove",
|
|
265
301
|
label: "Session Delegate Remove",
|
|
@@ -271,6 +307,7 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
271
307
|
parameters: DelegateStatusParams,
|
|
272
308
|
async execute(toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
273
309
|
const ok = await serverProxy.delegate_remove(params.sessionId);
|
|
310
|
+
pi.appendEntry("coordinator_remove", { sessionId: params.sessionId, ok });
|
|
274
311
|
return {
|
|
275
312
|
content: [{ type: "text" as const, text: ok ? `Task ${params.sessionId} removed.` : `Task ${params.sessionId} not found.` }],
|
|
276
313
|
details: { ok },
|
|
@@ -288,6 +325,7 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
288
325
|
parameters: Type.Object({}),
|
|
289
326
|
async execute(toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
290
327
|
const removed = await serverProxy.delegate_clear_stopped();
|
|
328
|
+
pi.appendEntry("coordinator_clear_stopped", { removed });
|
|
291
329
|
return {
|
|
292
330
|
content: [{ type: "text" as const, text: `Cleared ${removed} stopped/completed task(s).` }],
|
|
293
331
|
details: { removed },
|
|
@@ -307,6 +345,7 @@ export default function coordinatorExtension(pi: ExtensionAPI) {
|
|
|
307
345
|
const isCompletion = lowerMsg.includes("[completed]") || lowerMsg.includes("[done]") || lowerMsg.includes("task completed");
|
|
308
346
|
if (isCompletion) {
|
|
309
347
|
store.update(d.fromSessionId, { status: "completed", completedAt: Date.now(), result: d.message });
|
|
348
|
+
pi.appendEntry("coordinator_task_completed", { sessionId: d.fromSessionId, task: task.title, result: d.message.slice(0, 200) });
|
|
310
349
|
} else if (task.status !== "completed") {
|
|
311
350
|
store.update(d.fromSessionId, { status: "streaming" });
|
|
312
351
|
}
|
|
@@ -193,6 +193,9 @@ export default function hooksEngine(pi: ExtensionAPI): void {
|
|
|
193
193
|
|
|
194
194
|
if (promptResults.length > 0) {
|
|
195
195
|
console.log("[hook] Prompts to inject:", promptResults);
|
|
196
|
+
for (const prompt of promptResults) {
|
|
197
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
198
|
+
}
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
return undefined;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface SmartFileTrackerOptions {
|
|
4
|
+
maxOpenFiles?: number;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
priorityMap?: Record<string, number>;
|
|
7
|
+
excludedExtensions?: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MemoryUsage {
|
|
11
|
+
heapUsed: number;
|
|
12
|
+
heapTotal: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TrackerStatistics {
|
|
16
|
+
totalOpens: number;
|
|
17
|
+
totalEvictions: number;
|
|
18
|
+
evictionReasons: Array<"window_full" | "memory_pressure" | "excluded_type">;
|
|
19
|
+
accessCounts: Map<string, number>;
|
|
20
|
+
openFileCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TrackedFile {
|
|
24
|
+
filePath: string;
|
|
25
|
+
lastAccess: number;
|
|
26
|
+
modifiedTime: number;
|
|
27
|
+
accessCount: number;
|
|
28
|
+
priority: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Default priority map for common file types (higher = more important)
|
|
32
|
+
const DEFAULT_PRIORITY_MAP: Record<string, number> = {
|
|
33
|
+
// TypeScript/JavaScript (highest)
|
|
34
|
+
".ts": 100,
|
|
35
|
+
".tsx": 100,
|
|
36
|
+
".js": 90,
|
|
37
|
+
".jsx": 90,
|
|
38
|
+
".mjs": 90,
|
|
39
|
+
".cjs": 90,
|
|
40
|
+
|
|
41
|
+
// Framework files
|
|
42
|
+
".vue": 95,
|
|
43
|
+
".svelte": 95,
|
|
44
|
+
|
|
45
|
+
// Other languages
|
|
46
|
+
".py": 80,
|
|
47
|
+
".rs": 80,
|
|
48
|
+
".go": 80,
|
|
49
|
+
".java": 75,
|
|
50
|
+
".kt": 75,
|
|
51
|
+
".c": 70,
|
|
52
|
+
".cpp": 70,
|
|
53
|
+
".h": 70,
|
|
54
|
+
".hpp": 70,
|
|
55
|
+
".cs": 70,
|
|
56
|
+
|
|
57
|
+
// Config files
|
|
58
|
+
".json": 30,
|
|
59
|
+
".yaml": 30,
|
|
60
|
+
".yml": 30,
|
|
61
|
+
".toml": 30,
|
|
62
|
+
|
|
63
|
+
// Documentation
|
|
64
|
+
".md": 20,
|
|
65
|
+
|
|
66
|
+
// Text files
|
|
67
|
+
".txt": 10,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Default excluded file types (never tracked)
|
|
71
|
+
const DEFAULT_EXCLUDED_EXTENSIONS: Set<string> = new Set([
|
|
72
|
+
".log",
|
|
73
|
+
".bak",
|
|
74
|
+
".tmp",
|
|
75
|
+
".temp",
|
|
76
|
+
".swp",
|
|
77
|
+
".cache",
|
|
78
|
+
".DS_Store",
|
|
79
|
+
".map",
|
|
80
|
+
".lock",
|
|
81
|
+
".pid",
|
|
82
|
+
// Special files without extension
|
|
83
|
+
".gitignore",
|
|
84
|
+
".gitattributes",
|
|
85
|
+
".gitmodules",
|
|
86
|
+
".editorconfig",
|
|
87
|
+
".eslintrc",
|
|
88
|
+
".prettierrc",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// Memory thresholds (in bytes)
|
|
92
|
+
const HIGH_MEMORY_THRESHOLD = 3_000_000_000; // 3GB
|
|
93
|
+
const CRITICAL_MEMORY_THRESHOLD = 3_500_000_000; // 3.5GB
|
|
94
|
+
const BASE_MAX_FILES = 30;
|
|
95
|
+
const HIGH_MEMORY_MAX_FILES = 10;
|
|
96
|
+
const CRITICAL_MEMORY_MAX_FILES = 5;
|
|
97
|
+
|
|
98
|
+
export interface SmartFileTracker {
|
|
99
|
+
open(filePath: string, onClose: (file: string) => void, mtime?: number): void;
|
|
100
|
+
getOpenFiles(): string[];
|
|
101
|
+
closeAll(onClose: (file: string) => void): void;
|
|
102
|
+
updateMemoryUsage(usage: MemoryUsage): void;
|
|
103
|
+
getStatistics(): TrackerStatistics;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createSmartFileTracker(options: SmartFileTrackerOptions = {}): SmartFileTracker {
|
|
107
|
+
const baseMaxOpenFiles = options.maxOpenFiles ?? BASE_MAX_FILES;
|
|
108
|
+
const now = options.now ?? (() => Date.now());
|
|
109
|
+
const priorityMap = options.priorityMap ?? DEFAULT_PRIORITY_MAP;
|
|
110
|
+
const excludedExtensions = options.excludedExtensions ?? DEFAULT_EXCLUDED_EXTENSIONS;
|
|
111
|
+
|
|
112
|
+
const files = new Map<string, TrackedFile>();
|
|
113
|
+
const stats: TrackerStatistics = {
|
|
114
|
+
totalOpens: 0,
|
|
115
|
+
totalEvictions: 0,
|
|
116
|
+
evictionReasons: [],
|
|
117
|
+
accessCounts: new Map(),
|
|
118
|
+
openFileCount: 0,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let memoryUsage: MemoryUsage = {
|
|
122
|
+
heapUsed: 0,
|
|
123
|
+
heapTotal: 0,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Calculate current max files based on memory pressure
|
|
127
|
+
function getCurrentMaxFiles(): number {
|
|
128
|
+
if (!memoryUsage.heapUsed || !memoryUsage.heapTotal) {
|
|
129
|
+
return baseMaxOpenFiles;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const memoryRatio = memoryUsage.heapUsed / memoryUsage.heapTotal;
|
|
133
|
+
|
|
134
|
+
if (memoryRatio > CRITICAL_MEMORY_THRESHOLD / memoryUsage.heapTotal) {
|
|
135
|
+
return Math.min(CRITICAL_MEMORY_MAX_FILES, baseMaxOpenFiles);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (memoryRatio > HIGH_MEMORY_THRESHOLD / memoryUsage.heapTotal) {
|
|
139
|
+
return Math.min(HIGH_MEMORY_MAX_FILES, baseMaxOpenFiles);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return baseMaxOpenFiles;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get file priority based on extension
|
|
146
|
+
function getFilePriority(filePath: string): number {
|
|
147
|
+
const ext = extname(filePath).toLowerCase();
|
|
148
|
+
return priorityMap[ext] ?? 50; // Default priority for unknown types
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if file should be excluded
|
|
152
|
+
function isExcluded(filePath: string): boolean {
|
|
153
|
+
const ext = extname(filePath).toLowerCase();
|
|
154
|
+
if (excludedExtensions.has(ext)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check excluded file names (files without extension like .gitignore)
|
|
159
|
+
const basename = filePath.split("/").pop() ?? filePath;
|
|
160
|
+
if (excludedExtensions.has(basename)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sort files by priority (higher first), then by modified time (newer first), then by last access
|
|
168
|
+
function getSortedFiles(): TrackedFile[] {
|
|
169
|
+
return Array.from(files.values()).sort((a, b) => {
|
|
170
|
+
// Higher priority first
|
|
171
|
+
if (b.priority !== a.priority) {
|
|
172
|
+
return b.priority - a.priority;
|
|
173
|
+
}
|
|
174
|
+
// Newer modified time first
|
|
175
|
+
if (b.modifiedTime !== a.modifiedTime) {
|
|
176
|
+
return b.modifiedTime - a.modifiedTime;
|
|
177
|
+
}
|
|
178
|
+
// More recently accessed first
|
|
179
|
+
if (b.lastAccess !== a.lastAccess) {
|
|
180
|
+
return b.lastAccess - a.lastAccess;
|
|
181
|
+
}
|
|
182
|
+
// Higher access count first
|
|
183
|
+
return b.accessCount - a.accessCount;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Evict lowest priority file(s) to fit new file
|
|
188
|
+
function evictIfNeeded(onClose: (file: string) => void): void {
|
|
189
|
+
const currentMax = getCurrentMaxFiles();
|
|
190
|
+
// Evict if we're at or over the limit (before adding new file)
|
|
191
|
+
while (files.size >= currentMax) {
|
|
192
|
+
const sorted = getSortedFiles();
|
|
193
|
+
const lowest = sorted[sorted.length - 1]; // Last element has lowest priority
|
|
194
|
+
|
|
195
|
+
if (!lowest) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
files.delete(lowest.filePath);
|
|
200
|
+
onClose(lowest.filePath);
|
|
201
|
+
|
|
202
|
+
stats.totalEvictions++;
|
|
203
|
+
stats.evictionReasons.push("window_full");
|
|
204
|
+
stats.openFileCount = files.size;
|
|
205
|
+
|
|
206
|
+
// If memory is high, evict multiple files
|
|
207
|
+
const memoryRatio = memoryUsage.heapUsed / memoryUsage.heapTotal;
|
|
208
|
+
if (memoryRatio > HIGH_MEMORY_THRESHOLD / memoryUsage.heapTotal && files.size > currentMax / 2) {
|
|
209
|
+
continue; // Keep evicting
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Evict files to fit within memory constraints
|
|
217
|
+
function evictForMemory(onClose?: (file: string) => void): void {
|
|
218
|
+
const currentMax = getCurrentMaxFiles();
|
|
219
|
+
while (files.size > currentMax) {
|
|
220
|
+
const sorted = getSortedFiles();
|
|
221
|
+
const lowest = sorted[sorted.length - 1];
|
|
222
|
+
|
|
223
|
+
if (!lowest) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
files.delete(lowest.filePath);
|
|
228
|
+
if (onClose) {
|
|
229
|
+
onClose(lowest.filePath);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
stats.totalEvictions++;
|
|
233
|
+
stats.evictionReasons.push("memory_pressure");
|
|
234
|
+
stats.openFileCount = files.size;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
open(filePath: string, onClose: (file: string) => void, mtime?: number): void {
|
|
240
|
+
// Check if file is excluded
|
|
241
|
+
if (isExcluded(filePath)) {
|
|
242
|
+
onClose(filePath);
|
|
243
|
+
stats.totalEvictions++;
|
|
244
|
+
stats.evictionReasons.push("excluded_type");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
stats.totalOpens++;
|
|
249
|
+
|
|
250
|
+
// Update access count
|
|
251
|
+
const currentCount = stats.accessCounts.get(filePath) ?? 0;
|
|
252
|
+
stats.accessCounts.set(filePath, currentCount + 1);
|
|
253
|
+
|
|
254
|
+
// If file already exists, update its metadata
|
|
255
|
+
if (files.has(filePath)) {
|
|
256
|
+
const entry = files.get(filePath)!;
|
|
257
|
+
entry.lastAccess = now();
|
|
258
|
+
entry.modifiedTime = mtime ?? entry.modifiedTime;
|
|
259
|
+
entry.accessCount++;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Evict files if needed (before adding new file)
|
|
264
|
+
evictIfNeeded(onClose);
|
|
265
|
+
|
|
266
|
+
// Add new file
|
|
267
|
+
const newFile: TrackedFile = {
|
|
268
|
+
filePath,
|
|
269
|
+
lastAccess: now(),
|
|
270
|
+
modifiedTime: mtime ?? now(),
|
|
271
|
+
accessCount: 1,
|
|
272
|
+
priority: getFilePriority(filePath),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
files.set(filePath, newFile);
|
|
276
|
+
stats.openFileCount = files.size;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
getOpenFiles(): string[] {
|
|
280
|
+
const sorted = getSortedFiles();
|
|
281
|
+
return sorted.map((f) => f.filePath);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
closeAll(onClose: (file: string) => void): void {
|
|
285
|
+
for (const filePath of files.keys()) {
|
|
286
|
+
onClose(filePath);
|
|
287
|
+
}
|
|
288
|
+
files.clear();
|
|
289
|
+
stats.openFileCount = 0;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
updateMemoryUsage(usage: MemoryUsage): void {
|
|
293
|
+
memoryUsage = usage;
|
|
294
|
+
// Evict files if memory pressure increased window size
|
|
295
|
+
evictForMemory();
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
getStatistics(): TrackerStatistics {
|
|
299
|
+
return { ...stats, accessCounts: new Map(stats.accessCounts) };
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|