@tarquinen/opencode-dcp 0.3.16 โ 0.3.17
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 +18 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -43
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +1 -15
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +22 -95
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/deduplicator.d.ts +0 -30
- package/dist/lib/deduplicator.d.ts.map +1 -1
- package/dist/lib/deduplicator.js +1 -62
- package/dist/lib/deduplicator.js.map +1 -1
- package/dist/lib/janitor.d.ts +2 -61
- package/dist/lib/janitor.d.ts.map +1 -1
- package/dist/lib/janitor.js +48 -193
- package/dist/lib/janitor.js.map +1 -1
- package/dist/lib/logger.d.ts +0 -24
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +3 -58
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/model-selector.d.ts +0 -31
- package/dist/lib/model-selector.d.ts.map +1 -1
- package/dist/lib/model-selector.js +0 -55
- package/dist/lib/model-selector.js.map +1 -1
- package/dist/lib/prompt.d.ts.map +1 -1
- package/dist/lib/prompt.js +3 -29
- package/dist/lib/prompt.js.map +1 -1
- package/dist/lib/tokenizer.d.ts +0 -23
- package/dist/lib/tokenizer.d.ts.map +1 -1
- package/dist/lib/tokenizer.js +0 -25
- package/dist/lib/tokenizer.js.map +1 -1
- package/dist/lib/version-checker.d.ts +0 -15
- package/dist/lib/version-checker.d.ts.map +1 -1
- package/dist/lib/version-checker.js +1 -19
- package/dist/lib/version-checker.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/janitor.js
CHANGED
|
@@ -13,13 +13,10 @@ export class Janitor {
|
|
|
13
13
|
modelCache;
|
|
14
14
|
configModel;
|
|
15
15
|
showModelErrorToasts;
|
|
16
|
+
strictModelSelection;
|
|
16
17
|
pruningSummary;
|
|
17
18
|
workingDirectory;
|
|
18
|
-
constructor(client, prunedIdsState, statsState, logger, toolParametersCache, protectedTools, modelCache, configModel,
|
|
19
|
-
showModelErrorToasts = true, // Whether to show toast for model errors
|
|
20
|
-
pruningSummary = "detailed", // UI summary display mode
|
|
21
|
-
workingDirectory // Current working directory for relative path display
|
|
22
|
-
) {
|
|
19
|
+
constructor(client, prunedIdsState, statsState, logger, toolParametersCache, protectedTools, modelCache, configModel, showModelErrorToasts = true, strictModelSelection = false, pruningSummary = "detailed", workingDirectory) {
|
|
23
20
|
this.client = client;
|
|
24
21
|
this.prunedIdsState = prunedIdsState;
|
|
25
22
|
this.statsState = statsState;
|
|
@@ -29,20 +26,16 @@ export class Janitor {
|
|
|
29
26
|
this.modelCache = modelCache;
|
|
30
27
|
this.configModel = configModel;
|
|
31
28
|
this.showModelErrorToasts = showModelErrorToasts;
|
|
29
|
+
this.strictModelSelection = strictModelSelection;
|
|
32
30
|
this.pruningSummary = pruningSummary;
|
|
33
31
|
this.workingDirectory = workingDirectory;
|
|
34
32
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Sends an ignored message to the session UI (user sees it, AI doesn't)
|
|
37
|
-
*/
|
|
38
33
|
async sendIgnoredMessage(sessionID, text) {
|
|
39
34
|
try {
|
|
40
35
|
await this.client.session.prompt({
|
|
41
|
-
path: {
|
|
42
|
-
id: sessionID
|
|
43
|
-
},
|
|
36
|
+
path: { id: sessionID },
|
|
44
37
|
body: {
|
|
45
|
-
noReply: true,
|
|
38
|
+
noReply: true,
|
|
46
39
|
parts: [{
|
|
47
40
|
type: 'text',
|
|
48
41
|
text: text,
|
|
@@ -52,82 +45,56 @@ export class Janitor {
|
|
|
52
45
|
});
|
|
53
46
|
}
|
|
54
47
|
catch (error) {
|
|
55
|
-
this.logger.error("janitor", "Failed to send notification", {
|
|
56
|
-
error: error.message
|
|
57
|
-
});
|
|
48
|
+
this.logger.error("janitor", "Failed to send notification", { error: error.message });
|
|
58
49
|
}
|
|
59
50
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Convenience method for idle-triggered pruning (sends notification automatically)
|
|
62
|
-
*/
|
|
63
51
|
async runOnIdle(sessionID, strategies) {
|
|
64
52
|
await this.runWithStrategies(sessionID, strategies, { trigger: 'idle' });
|
|
65
|
-
// Notification is handled inside runWithStrategies
|
|
66
53
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Convenience method for tool-triggered pruning (returns result for tool output)
|
|
69
|
-
*/
|
|
70
54
|
async runForTool(sessionID, strategies, reason) {
|
|
71
55
|
return await this.runWithStrategies(sessionID, strategies, { trigger: 'tool', reason });
|
|
72
56
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Core pruning method that accepts strategies and options
|
|
75
|
-
*/
|
|
76
57
|
async runWithStrategies(sessionID, strategies, options) {
|
|
77
58
|
try {
|
|
78
|
-
// Skip if no strategies configured
|
|
79
59
|
if (strategies.length === 0) {
|
|
80
60
|
return null;
|
|
81
61
|
}
|
|
82
|
-
// Fetch session info and messages from OpenCode API
|
|
83
62
|
const [sessionInfoResponse, messagesResponse] = await Promise.all([
|
|
84
63
|
this.client.session.get({ path: { id: sessionID } }),
|
|
85
64
|
this.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } })
|
|
86
65
|
]);
|
|
87
66
|
const sessionInfo = sessionInfoResponse.data;
|
|
88
|
-
// Handle the response format - it should be { data: Array<{info, parts}> } or just the array
|
|
89
67
|
const messages = messagesResponse.data || messagesResponse;
|
|
90
|
-
// If there are no messages or very few, skip analysis
|
|
91
68
|
if (!messages || messages.length < 3) {
|
|
92
69
|
return null;
|
|
93
70
|
}
|
|
94
|
-
// Extract tool call IDs from the session and track their output sizes
|
|
95
|
-
// Also track batch tool relationships and tool metadata
|
|
96
71
|
const toolCallIds = [];
|
|
97
72
|
const toolOutputs = new Map();
|
|
98
|
-
const toolMetadata = new Map();
|
|
99
|
-
const batchToolChildren = new Map();
|
|
73
|
+
const toolMetadata = new Map();
|
|
74
|
+
const batchToolChildren = new Map();
|
|
100
75
|
let currentBatchId = null;
|
|
101
76
|
for (const msg of messages) {
|
|
102
77
|
if (msg.parts) {
|
|
103
78
|
for (const part of msg.parts) {
|
|
104
79
|
if (part.type === "tool" && part.callID) {
|
|
105
|
-
// Normalize tool call IDs to lowercase for consistent comparison
|
|
106
80
|
const normalizedId = part.callID.toLowerCase();
|
|
107
81
|
toolCallIds.push(normalizedId);
|
|
108
|
-
// Try to get parameters from cache first, fall back to part.parameters
|
|
109
|
-
// Cache might have either case, so check both
|
|
110
82
|
const cachedData = this.toolParametersCache.get(part.callID) || this.toolParametersCache.get(normalizedId);
|
|
111
83
|
const parameters = cachedData?.parameters || part.parameters;
|
|
112
|
-
// Track tool metadata (name and parameters)
|
|
113
84
|
toolMetadata.set(normalizedId, {
|
|
114
85
|
tool: part.tool,
|
|
115
86
|
parameters: parameters
|
|
116
87
|
});
|
|
117
|
-
// Track the output content for size calculation
|
|
118
88
|
if (part.state?.status === "completed" && part.state.output) {
|
|
119
89
|
toolOutputs.set(normalizedId, part.state.output);
|
|
120
90
|
}
|
|
121
|
-
// Check if this is a batch tool by looking at the tool name
|
|
122
91
|
if (part.tool === "batch") {
|
|
123
92
|
currentBatchId = normalizedId;
|
|
124
93
|
batchToolChildren.set(normalizedId, []);
|
|
125
94
|
}
|
|
126
|
-
// If we're inside a batch and this is a prt_ (parallel) tool call, it's a child
|
|
127
95
|
else if (currentBatchId && normalizedId.startsWith('prt_')) {
|
|
128
96
|
batchToolChildren.get(currentBatchId).push(normalizedId);
|
|
129
97
|
}
|
|
130
|
-
// If we hit a non-batch, non-prt_ tool, we're out of the batch
|
|
131
98
|
else if (currentBatchId && !normalizedId.startsWith('prt_')) {
|
|
132
99
|
currentBatchId = null;
|
|
133
100
|
}
|
|
@@ -135,16 +102,12 @@ export class Janitor {
|
|
|
135
102
|
}
|
|
136
103
|
}
|
|
137
104
|
}
|
|
138
|
-
// Get already pruned IDs to filter them out
|
|
139
105
|
const alreadyPrunedIds = this.prunedIdsState.get(sessionID) ?? [];
|
|
140
106
|
const unprunedToolCallIds = toolCallIds.filter(id => !alreadyPrunedIds.includes(id));
|
|
141
|
-
// If there are no unpruned tool calls, skip analysis
|
|
142
107
|
if (unprunedToolCallIds.length === 0) {
|
|
143
108
|
return null;
|
|
144
109
|
}
|
|
145
|
-
//
|
|
146
|
-
// PHASE 1: DUPLICATE DETECTION (if enabled)
|
|
147
|
-
// ============================================================
|
|
110
|
+
// PHASE 1: DUPLICATE DETECTION
|
|
148
111
|
let deduplicatedIds = [];
|
|
149
112
|
let deduplicationDetails = new Map();
|
|
150
113
|
if (strategies.includes('deduplication')) {
|
|
@@ -152,23 +115,17 @@ export class Janitor {
|
|
|
152
115
|
deduplicatedIds = dedupeResult.duplicateIds;
|
|
153
116
|
deduplicationDetails = dedupeResult.deduplicationDetails;
|
|
154
117
|
}
|
|
155
|
-
// Calculate candidates available for pruning (excludes protected tools)
|
|
156
118
|
const candidateCount = unprunedToolCallIds.filter(id => {
|
|
157
119
|
const metadata = toolMetadata.get(id);
|
|
158
120
|
return !metadata || !this.protectedTools.includes(metadata.tool);
|
|
159
121
|
}).length;
|
|
160
|
-
//
|
|
161
|
-
// PHASE 2: LLM ANALYSIS (if enabled)
|
|
162
|
-
// ============================================================
|
|
122
|
+
// PHASE 2: LLM ANALYSIS
|
|
163
123
|
let llmPrunedIds = [];
|
|
164
124
|
if (strategies.includes('ai-analysis')) {
|
|
165
|
-
// Filter out duplicates and protected tools
|
|
166
125
|
const protectedToolCallIds = [];
|
|
167
126
|
const prunableToolCallIds = unprunedToolCallIds.filter(id => {
|
|
168
|
-
// Skip already deduplicated
|
|
169
127
|
if (deduplicatedIds.includes(id))
|
|
170
128
|
return false;
|
|
171
|
-
// Skip protected tools
|
|
172
129
|
const metadata = toolMetadata.get(id);
|
|
173
130
|
if (metadata && this.protectedTools.includes(metadata.tool)) {
|
|
174
131
|
protectedToolCallIds.push(id);
|
|
@@ -176,9 +133,7 @@ export class Janitor {
|
|
|
176
133
|
}
|
|
177
134
|
return true;
|
|
178
135
|
});
|
|
179
|
-
// Run LLM analysis only if there are prunable tools
|
|
180
136
|
if (prunableToolCallIds.length > 0) {
|
|
181
|
-
// Select appropriate model with intelligent fallback
|
|
182
137
|
const cachedModelInfo = this.modelCache.get(sessionID);
|
|
183
138
|
const sessionModelInfo = extractModelFromSession(sessionInfo, this.logger);
|
|
184
139
|
const currentModelInfo = cachedModelInfo || sessionModelInfo;
|
|
@@ -186,73 +141,68 @@ export class Janitor {
|
|
|
186
141
|
this.logger.info("janitor", `Model: ${modelSelection.modelInfo.providerID}/${modelSelection.modelInfo.modelID}`, {
|
|
187
142
|
source: modelSelection.source
|
|
188
143
|
});
|
|
189
|
-
// Show toast if we had to fallback from a failed model
|
|
190
144
|
if (modelSelection.failedModel && this.showModelErrorToasts) {
|
|
145
|
+
const skipAi = modelSelection.source === 'fallback' && this.strictModelSelection;
|
|
191
146
|
try {
|
|
192
147
|
await this.client.tui.showToast({
|
|
193
148
|
body: {
|
|
194
|
-
title: "DCP: Model fallback",
|
|
195
|
-
message:
|
|
149
|
+
title: skipAi ? "DCP: AI analysis skipped" : "DCP: Model fallback",
|
|
150
|
+
message: skipAi
|
|
151
|
+
? `${modelSelection.failedModel.providerID}/${modelSelection.failedModel.modelID} failed\nAI analysis skipped (strictModelSelection enabled)`
|
|
152
|
+
: `${modelSelection.failedModel.providerID}/${modelSelection.failedModel.modelID} failed\nUsing ${modelSelection.modelInfo.providerID}/${modelSelection.modelInfo.modelID}`,
|
|
196
153
|
variant: "info",
|
|
197
154
|
duration: 5000
|
|
198
155
|
}
|
|
199
156
|
});
|
|
200
157
|
}
|
|
201
158
|
catch (toastError) {
|
|
202
|
-
// Don't fail the whole operation if toast fails
|
|
203
159
|
}
|
|
204
160
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const reasoning = result.object.reasoning.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
238
|
-
this.logger.info("janitor", `LLM reasoning: ${reasoning.substring(0, 200)}${reasoning.length > 200 ? '...' : ''}`);
|
|
161
|
+
if (modelSelection.source === 'fallback' && this.strictModelSelection) {
|
|
162
|
+
this.logger.info("janitor", "Skipping AI analysis (fallback model, strictModelSelection enabled)");
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const { generateObject } = await import('ai');
|
|
166
|
+
const allPrunedSoFar = [...alreadyPrunedIds, ...deduplicatedIds];
|
|
167
|
+
const sanitizedMessages = this.replacePrunedToolOutputs(messages, allPrunedSoFar);
|
|
168
|
+
const analysisPrompt = buildAnalysisPrompt(prunableToolCallIds, sanitizedMessages, allPrunedSoFar, protectedToolCallIds, options.reason);
|
|
169
|
+
await this.logger.saveWrappedContext("janitor-shadow", [{ role: "user", content: analysisPrompt }], {
|
|
170
|
+
sessionID,
|
|
171
|
+
modelProvider: modelSelection.modelInfo.providerID,
|
|
172
|
+
modelID: modelSelection.modelInfo.modelID,
|
|
173
|
+
candidateToolCount: prunableToolCallIds.length,
|
|
174
|
+
alreadyPrunedCount: allPrunedSoFar.length,
|
|
175
|
+
protectedToolCount: protectedToolCallIds.length,
|
|
176
|
+
trigger: options.trigger,
|
|
177
|
+
reason: options.reason
|
|
178
|
+
});
|
|
179
|
+
const result = await generateObject({
|
|
180
|
+
model: modelSelection.model,
|
|
181
|
+
schema: z.object({
|
|
182
|
+
pruned_tool_call_ids: z.array(z.string()),
|
|
183
|
+
reasoning: z.string(),
|
|
184
|
+
}),
|
|
185
|
+
prompt: analysisPrompt
|
|
186
|
+
});
|
|
187
|
+
const rawLlmPrunedIds = result.object.pruned_tool_call_ids;
|
|
188
|
+
llmPrunedIds = rawLlmPrunedIds.filter(id => prunableToolCallIds.includes(id.toLowerCase()));
|
|
189
|
+
if (llmPrunedIds.length > 0) {
|
|
190
|
+
const reasoning = result.object.reasoning.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
191
|
+
this.logger.info("janitor", `LLM reasoning: ${reasoning.substring(0, 200)}${reasoning.length > 200 ? '...' : ''}`);
|
|
192
|
+
}
|
|
239
193
|
}
|
|
240
194
|
}
|
|
241
195
|
}
|
|
242
|
-
// ============================================================
|
|
243
196
|
// PHASE 3: COMBINE & EXPAND
|
|
244
|
-
// ============================================================
|
|
245
197
|
const newlyPrunedIds = [...deduplicatedIds, ...llmPrunedIds];
|
|
246
198
|
if (newlyPrunedIds.length === 0) {
|
|
247
199
|
return null;
|
|
248
200
|
}
|
|
249
|
-
// Helper to expand batch tool IDs to include their children
|
|
250
201
|
const expandBatchIds = (ids) => {
|
|
251
202
|
const expanded = new Set();
|
|
252
203
|
for (const id of ids) {
|
|
253
204
|
const normalizedId = id.toLowerCase();
|
|
254
205
|
expanded.add(normalizedId);
|
|
255
|
-
// If this is a batch tool, add all its children
|
|
256
206
|
const children = batchToolChildren.get(normalizedId);
|
|
257
207
|
if (children) {
|
|
258
208
|
children.forEach(childId => expanded.add(childId));
|
|
@@ -260,27 +210,18 @@ export class Janitor {
|
|
|
260
210
|
}
|
|
261
211
|
return Array.from(expanded);
|
|
262
212
|
};
|
|
263
|
-
// Expand batch tool IDs to include their children
|
|
264
213
|
const expandedPrunedIds = new Set(expandBatchIds(newlyPrunedIds));
|
|
265
|
-
// Expand llmPrunedIds for UI display (so batch children show instead of "unknown metadata")
|
|
266
214
|
const expandedLlmPrunedIds = expandBatchIds(llmPrunedIds);
|
|
267
|
-
// Calculate which IDs are actually NEW (not already pruned)
|
|
268
215
|
const finalNewlyPrunedIds = Array.from(expandedPrunedIds).filter(id => !alreadyPrunedIds.includes(id));
|
|
269
|
-
// finalPrunedIds includes everything (new + already pruned) for logging
|
|
270
216
|
const finalPrunedIds = Array.from(expandedPrunedIds);
|
|
271
|
-
// ============================================================
|
|
272
217
|
// PHASE 4: CALCULATE STATS & NOTIFICATION
|
|
273
|
-
// ============================================================
|
|
274
|
-
// Calculate token savings once (used by both notification and log)
|
|
275
218
|
const tokensSaved = await this.calculateTokensSaved(finalNewlyPrunedIds, toolOutputs);
|
|
276
|
-
// Accumulate session stats (for showing cumulative totals in UI)
|
|
277
219
|
const currentStats = this.statsState.get(sessionID) ?? { totalToolsPruned: 0, totalTokensSaved: 0 };
|
|
278
220
|
const sessionStats = {
|
|
279
221
|
totalToolsPruned: currentStats.totalToolsPruned + finalNewlyPrunedIds.length,
|
|
280
222
|
totalTokensSaved: currentStats.totalTokensSaved + tokensSaved
|
|
281
223
|
};
|
|
282
224
|
this.statsState.set(sessionID, sessionStats);
|
|
283
|
-
// Determine notification mode based on which strategies ran
|
|
284
225
|
const hasLlmAnalysis = strategies.includes('ai-analysis');
|
|
285
226
|
if (hasLlmAnalysis) {
|
|
286
227
|
await this.sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, expandedLlmPrunedIds, toolMetadata, tokensSaved, sessionStats);
|
|
@@ -288,19 +229,13 @@ export class Janitor {
|
|
|
288
229
|
else {
|
|
289
230
|
await this.sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails, tokensSaved, sessionStats);
|
|
290
231
|
}
|
|
291
|
-
// ============================================================
|
|
292
232
|
// PHASE 5: STATE UPDATE
|
|
293
|
-
// ============================================================
|
|
294
|
-
// Merge newly pruned IDs with existing ones (using expanded IDs)
|
|
295
233
|
const allPrunedIds = [...new Set([...alreadyPrunedIds, ...finalPrunedIds])];
|
|
296
234
|
this.prunedIdsState.set(sessionID, allPrunedIds);
|
|
297
|
-
// Log final summary
|
|
298
|
-
// Format: "Pruned 5/5 tools (~4.2K tokens), 0 kept" or with breakdown if both duplicate and llm
|
|
299
235
|
const prunedCount = finalNewlyPrunedIds.length;
|
|
300
236
|
const keptCount = candidateCount - prunedCount;
|
|
301
237
|
const hasBoth = deduplicatedIds.length > 0 && llmPrunedIds.length > 0;
|
|
302
238
|
const breakdown = hasBoth ? ` (${deduplicatedIds.length} duplicate, ${llmPrunedIds.length} llm)` : "";
|
|
303
|
-
// Build log metadata
|
|
304
239
|
const logMeta = { trigger: options.trigger };
|
|
305
240
|
if (options.reason) {
|
|
306
241
|
logMeta.reason = options.reason;
|
|
@@ -321,17 +256,10 @@ export class Janitor {
|
|
|
321
256
|
error: error.message,
|
|
322
257
|
trigger: options.trigger
|
|
323
258
|
});
|
|
324
|
-
// Don't throw - this is a fire-and-forget background process
|
|
325
|
-
// Silently fail and try again on next idle event
|
|
326
259
|
return null;
|
|
327
260
|
}
|
|
328
261
|
}
|
|
329
|
-
/**
|
|
330
|
-
* Helper function to shorten paths for display
|
|
331
|
-
*/
|
|
332
262
|
shortenPath(input) {
|
|
333
|
-
// Handle compound strings like: "pattern" in /absolute/path
|
|
334
|
-
// Extract and shorten just the path portion
|
|
335
263
|
const inPathMatch = input.match(/^(.+) in (.+)$/);
|
|
336
264
|
if (inPathMatch) {
|
|
337
265
|
const prefix = inPathMatch[1];
|
|
@@ -341,31 +269,23 @@ export class Janitor {
|
|
|
341
269
|
}
|
|
342
270
|
return this.shortenSinglePath(input);
|
|
343
271
|
}
|
|
344
|
-
/**
|
|
345
|
-
* Shorten a single path string
|
|
346
|
-
*/
|
|
347
272
|
shortenSinglePath(path) {
|
|
348
273
|
const homeDir = require('os').homedir();
|
|
349
|
-
// Strip working directory FIRST (before ~ replacement) for cleaner relative paths
|
|
350
274
|
if (this.workingDirectory) {
|
|
351
275
|
if (path.startsWith(this.workingDirectory + '/')) {
|
|
352
276
|
return path.slice(this.workingDirectory.length + 1);
|
|
353
277
|
}
|
|
354
|
-
// Exact match (the directory itself)
|
|
355
278
|
if (path === this.workingDirectory) {
|
|
356
279
|
return '.';
|
|
357
280
|
}
|
|
358
281
|
}
|
|
359
|
-
// Replace home directory with ~
|
|
360
282
|
if (path.startsWith(homeDir)) {
|
|
361
283
|
path = '~' + path.slice(homeDir.length);
|
|
362
284
|
}
|
|
363
|
-
// Shorten node_modules paths: show package + file only
|
|
364
285
|
const nodeModulesMatch = path.match(/node_modules\/(@[^\/]+\/[^\/]+|[^\/]+)\/(.*)/);
|
|
365
286
|
if (nodeModulesMatch) {
|
|
366
287
|
return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}`;
|
|
367
288
|
}
|
|
368
|
-
// Try matching against ~ version of working directory (for paths already with ~)
|
|
369
289
|
if (this.workingDirectory) {
|
|
370
290
|
const workingDirWithTilde = this.workingDirectory.startsWith(homeDir)
|
|
371
291
|
? '~' + this.workingDirectory.slice(homeDir.length)
|
|
@@ -379,11 +299,6 @@ export class Janitor {
|
|
|
379
299
|
}
|
|
380
300
|
return path;
|
|
381
301
|
}
|
|
382
|
-
/**
|
|
383
|
-
* Replace pruned tool outputs with placeholder text to save tokens in janitor context
|
|
384
|
-
* This applies the same replacement logic as the global fetch wrapper, but for the
|
|
385
|
-
* janitor's shadow inference to avoid sending already-pruned content to the LLM
|
|
386
|
-
*/
|
|
387
302
|
replacePrunedToolOutputs(messages, prunedIds) {
|
|
388
303
|
if (prunedIds.length === 0)
|
|
389
304
|
return messages;
|
|
@@ -398,7 +313,6 @@ export class Janitor {
|
|
|
398
313
|
part.callID &&
|
|
399
314
|
prunedIdsSet.has(part.callID.toLowerCase()) &&
|
|
400
315
|
part.state?.output) {
|
|
401
|
-
// Replace with the same placeholder used by the global fetch wrapper
|
|
402
316
|
return {
|
|
403
317
|
...part,
|
|
404
318
|
state: {
|
|
@@ -412,9 +326,6 @@ export class Janitor {
|
|
|
412
326
|
};
|
|
413
327
|
});
|
|
414
328
|
}
|
|
415
|
-
/**
|
|
416
|
-
* Helper function to calculate token savings from tool outputs
|
|
417
|
-
*/
|
|
418
329
|
async calculateTokensSaved(prunedIds, toolOutputs) {
|
|
419
330
|
const outputsToTokenize = [];
|
|
420
331
|
for (const prunedId of prunedIds) {
|
|
@@ -424,63 +335,44 @@ export class Janitor {
|
|
|
424
335
|
}
|
|
425
336
|
}
|
|
426
337
|
if (outputsToTokenize.length > 0) {
|
|
427
|
-
// Use batch tokenization for efficiency (lazy loads gpt-tokenizer)
|
|
428
338
|
const tokenCounts = await estimateTokensBatch(outputsToTokenize);
|
|
429
339
|
return tokenCounts.reduce((sum, count) => sum + count, 0);
|
|
430
340
|
}
|
|
431
341
|
return 0;
|
|
432
342
|
}
|
|
433
|
-
/**
|
|
434
|
-
* Build a summary of tools by grouping them
|
|
435
|
-
* Uses shared extractParameterKey logic for consistent parameter extraction
|
|
436
|
-
*
|
|
437
|
-
* Note: prunedIds may be in original case (from LLM) but toolMetadata uses lowercase keys
|
|
438
|
-
*/
|
|
439
343
|
buildToolsSummary(prunedIds, toolMetadata) {
|
|
440
344
|
const toolsSummary = new Map();
|
|
441
|
-
// Helper function to truncate long strings
|
|
442
345
|
const truncate = (str, maxLen = 60) => {
|
|
443
346
|
if (str.length <= maxLen)
|
|
444
347
|
return str;
|
|
445
348
|
return str.slice(0, maxLen - 3) + '...';
|
|
446
349
|
};
|
|
447
350
|
for (const prunedId of prunedIds) {
|
|
448
|
-
// Normalize ID to lowercase for lookup (toolMetadata uses lowercase keys)
|
|
449
351
|
const normalizedId = prunedId.toLowerCase();
|
|
450
352
|
const metadata = toolMetadata.get(normalizedId);
|
|
451
353
|
if (metadata) {
|
|
452
354
|
const toolName = metadata.tool;
|
|
453
|
-
// Skip 'batch' tool in UI summary - it's a wrapper and its children are shown individually
|
|
454
355
|
if (toolName === 'batch')
|
|
455
356
|
continue;
|
|
456
357
|
if (!toolsSummary.has(toolName)) {
|
|
457
358
|
toolsSummary.set(toolName, []);
|
|
458
359
|
}
|
|
459
|
-
// Use shared parameter extraction logic
|
|
460
360
|
const paramKey = extractParameterKey(metadata);
|
|
461
361
|
if (paramKey) {
|
|
462
|
-
// Apply path shortening and truncation for display
|
|
463
362
|
const displayKey = truncate(this.shortenPath(paramKey), 80);
|
|
464
363
|
toolsSummary.get(toolName).push(displayKey);
|
|
465
364
|
}
|
|
466
365
|
else {
|
|
467
|
-
// For tools with no extractable parameter key, add a placeholder
|
|
468
|
-
// This ensures the tool still shows up in the summary
|
|
469
366
|
toolsSummary.get(toolName).push('(default)');
|
|
470
367
|
}
|
|
471
368
|
}
|
|
472
369
|
}
|
|
473
370
|
return toolsSummary;
|
|
474
371
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Group deduplication details by tool type
|
|
477
|
-
* Shared helper used by notifications and tool output formatting
|
|
478
|
-
*/
|
|
479
372
|
groupDeduplicationDetails(deduplicationDetails) {
|
|
480
373
|
const grouped = new Map();
|
|
481
374
|
for (const [_, details] of deduplicationDetails) {
|
|
482
375
|
const { toolName, parameterKey, duplicateCount } = details;
|
|
483
|
-
// Skip 'batch' tool in UI summary - it's a wrapper and its children are shown individually
|
|
484
376
|
if (toolName === 'batch')
|
|
485
377
|
continue;
|
|
486
378
|
if (!grouped.has(toolName)) {
|
|
@@ -493,10 +385,6 @@ export class Janitor {
|
|
|
493
385
|
}
|
|
494
386
|
return grouped;
|
|
495
387
|
}
|
|
496
|
-
/**
|
|
497
|
-
* Format grouped deduplication results as lines
|
|
498
|
-
* Shared helper for building deduplication summaries
|
|
499
|
-
*/
|
|
500
388
|
formatDeduplicationLines(grouped, indent = ' ') {
|
|
501
389
|
const lines = [];
|
|
502
390
|
for (const [toolName, items] of grouped.entries()) {
|
|
@@ -507,10 +395,6 @@ export class Janitor {
|
|
|
507
395
|
}
|
|
508
396
|
return lines;
|
|
509
397
|
}
|
|
510
|
-
/**
|
|
511
|
-
* Format tool summary (from buildToolsSummary) as lines
|
|
512
|
-
* Shared helper for building LLM-pruned summaries
|
|
513
|
-
*/
|
|
514
398
|
formatToolSummaryLines(toolsSummary, indent = ' ') {
|
|
515
399
|
const lines = [];
|
|
516
400
|
for (const [toolName, params] of toolsSummary.entries()) {
|
|
@@ -526,47 +410,34 @@ export class Janitor {
|
|
|
526
410
|
}
|
|
527
411
|
return lines;
|
|
528
412
|
}
|
|
529
|
-
/**
|
|
530
|
-
* Send minimal summary notification (just tokens saved and count)
|
|
531
|
-
*/
|
|
532
413
|
async sendMinimalNotification(sessionID, totalPruned, tokensSaved, sessionStats) {
|
|
533
414
|
if (totalPruned === 0)
|
|
534
415
|
return;
|
|
535
416
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
536
417
|
const toolText = totalPruned === 1 ? 'tool' : 'tools';
|
|
537
418
|
let message = `๐งน DCP: Saved ~${tokensFormatted} tokens (${totalPruned} ${toolText} pruned)`;
|
|
538
|
-
// Add session totals if there's been more than one pruning run
|
|
539
419
|
if (sessionStats.totalToolsPruned > totalPruned) {
|
|
540
420
|
message += ` โ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
541
421
|
}
|
|
542
422
|
await this.sendIgnoredMessage(sessionID, message);
|
|
543
423
|
}
|
|
544
|
-
/**
|
|
545
|
-
* Auto mode notification - shows only deduplication results
|
|
546
|
-
*/
|
|
547
424
|
async sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails, tokensSaved, sessionStats) {
|
|
548
425
|
if (deduplicatedIds.length === 0)
|
|
549
426
|
return;
|
|
550
|
-
// Check if notifications are disabled
|
|
551
427
|
if (this.pruningSummary === 'off')
|
|
552
428
|
return;
|
|
553
|
-
// Send minimal notification if configured
|
|
554
429
|
if (this.pruningSummary === 'minimal') {
|
|
555
430
|
await this.sendMinimalNotification(sessionID, deduplicatedIds.length, tokensSaved, sessionStats);
|
|
556
431
|
return;
|
|
557
432
|
}
|
|
558
|
-
// Otherwise send detailed notification
|
|
559
433
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
560
434
|
const toolText = deduplicatedIds.length === 1 ? 'tool' : 'tools';
|
|
561
435
|
let message = `๐งน DCP: Saved ~${tokensFormatted} tokens (${deduplicatedIds.length} duplicate ${toolText} removed)`;
|
|
562
|
-
// Add session totals if there's been more than one pruning run
|
|
563
436
|
if (sessionStats.totalToolsPruned > deduplicatedIds.length) {
|
|
564
437
|
message += ` โ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
565
438
|
}
|
|
566
439
|
message += '\n';
|
|
567
|
-
// Group by tool type using shared helper
|
|
568
440
|
const grouped = this.groupDeduplicationDetails(deduplicationDetails);
|
|
569
|
-
// Display grouped results (with UI-specific formatting: total dupes header, limit to 5)
|
|
570
441
|
for (const [toolName, items] of grouped.entries()) {
|
|
571
442
|
const totalDupes = items.reduce((sum, item) => sum + (item.count - 1), 0);
|
|
572
443
|
message += `\n${toolName} (${totalDupes} duplicate${totalDupes > 1 ? 's' : ''}):\n`;
|
|
@@ -580,22 +451,16 @@ export class Janitor {
|
|
|
580
451
|
}
|
|
581
452
|
await this.sendIgnoredMessage(sessionID, message.trim());
|
|
582
453
|
}
|
|
583
|
-
/**
|
|
584
|
-
* Format pruning result for tool output (returned to AI)
|
|
585
|
-
* Uses shared helpers for consistency with UI notifications
|
|
586
|
-
*/
|
|
587
454
|
formatPruningResultForTool(result) {
|
|
588
455
|
const lines = [];
|
|
589
456
|
lines.push(`Context pruning complete. Pruned ${result.prunedCount} tool outputs.`);
|
|
590
457
|
lines.push('');
|
|
591
|
-
// Section 1: Deduplicated tools
|
|
592
458
|
if (result.deduplicatedIds.length > 0 && result.deduplicationDetails.size > 0) {
|
|
593
459
|
lines.push(`Duplicates removed (${result.deduplicatedIds.length}):`);
|
|
594
460
|
const grouped = this.groupDeduplicationDetails(result.deduplicationDetails);
|
|
595
461
|
lines.push(...this.formatDeduplicationLines(grouped));
|
|
596
462
|
lines.push('');
|
|
597
463
|
}
|
|
598
|
-
// Section 2: LLM-pruned tools
|
|
599
464
|
if (result.llmPrunedIds.length > 0) {
|
|
600
465
|
lines.push(`Semantically pruned (${result.llmPrunedIds.length}):`);
|
|
601
466
|
const toolsSummary = this.buildToolsSummary(result.llmPrunedIds, result.toolMetadata);
|
|
@@ -603,42 +468,33 @@ export class Janitor {
|
|
|
603
468
|
}
|
|
604
469
|
return lines.join('\n').trim();
|
|
605
470
|
}
|
|
606
|
-
/**
|
|
607
|
-
* Smart mode notification - shows both deduplication and LLM analysis results
|
|
608
|
-
*/
|
|
609
471
|
async sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, llmPrunedIds, toolMetadata, tokensSaved, sessionStats) {
|
|
610
472
|
const totalPruned = deduplicatedIds.length + llmPrunedIds.length;
|
|
611
473
|
if (totalPruned === 0)
|
|
612
474
|
return;
|
|
613
|
-
// Check if notifications are disabled
|
|
614
475
|
if (this.pruningSummary === 'off')
|
|
615
476
|
return;
|
|
616
|
-
// Send minimal notification if configured
|
|
617
477
|
if (this.pruningSummary === 'minimal') {
|
|
618
478
|
await this.sendMinimalNotification(sessionID, totalPruned, tokensSaved, sessionStats);
|
|
619
479
|
return;
|
|
620
480
|
}
|
|
621
|
-
// Otherwise send detailed notification
|
|
622
481
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
623
482
|
let message = `๐งน DCP: Saved ~${tokensFormatted} tokens (${totalPruned} tool${totalPruned > 1 ? 's' : ''} pruned)`;
|
|
624
|
-
// Add session totals if there's been more than one pruning run
|
|
625
483
|
if (sessionStats.totalToolsPruned > totalPruned) {
|
|
626
484
|
message += ` โ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
627
485
|
}
|
|
628
486
|
message += '\n';
|
|
629
|
-
// Section 1: Deduplicated tools
|
|
630
487
|
if (deduplicatedIds.length > 0 && deduplicationDetails) {
|
|
631
488
|
message += `\n๐ฆ Duplicates removed (${deduplicatedIds.length}):\n`;
|
|
632
489
|
const grouped = this.groupDeduplicationDetails(deduplicationDetails);
|
|
633
490
|
for (const [toolName, items] of grouped.entries()) {
|
|
634
491
|
message += ` ${toolName}:\n`;
|
|
635
492
|
for (const item of items) {
|
|
636
|
-
const removedCount = item.count - 1;
|
|
493
|
+
const removedCount = item.count - 1;
|
|
637
494
|
message += ` ${item.key} (${removedCount}ร duplicate)\n`;
|
|
638
495
|
}
|
|
639
496
|
}
|
|
640
497
|
}
|
|
641
|
-
// Section 2: LLM-pruned tools
|
|
642
498
|
if (llmPrunedIds.length > 0) {
|
|
643
499
|
message += `\n๐ค LLM analysis (${llmPrunedIds.length}):\n`;
|
|
644
500
|
const toolsSummary = this.buildToolsSummary(llmPrunedIds, toolMetadata);
|
|
@@ -650,7 +506,6 @@ export class Janitor {
|
|
|
650
506
|
}
|
|
651
507
|
}
|
|
652
508
|
}
|
|
653
|
-
// Handle any tools that weren't found in metadata (edge case)
|
|
654
509
|
const foundToolNames = new Set(toolsSummary.keys());
|
|
655
510
|
const missingTools = llmPrunedIds.filter(id => {
|
|
656
511
|
const normalizedId = id.toLowerCase();
|