@tarquinen/opencode-dcp 0.3.15 โ 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 +39 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -60
- 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 +67 -199
- 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,114 +141,101 @@ 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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
201
|
+
const expandBatchIds = (ids) => {
|
|
202
|
+
const expanded = new Set();
|
|
203
|
+
for (const id of ids) {
|
|
204
|
+
const normalizedId = id.toLowerCase();
|
|
205
|
+
expanded.add(normalizedId);
|
|
206
|
+
const children = batchToolChildren.get(normalizedId);
|
|
207
|
+
if (children) {
|
|
208
|
+
children.forEach(childId => expanded.add(childId));
|
|
209
|
+
}
|
|
258
210
|
}
|
|
259
|
-
|
|
260
|
-
|
|
211
|
+
return Array.from(expanded);
|
|
212
|
+
};
|
|
213
|
+
const expandedPrunedIds = new Set(expandBatchIds(newlyPrunedIds));
|
|
214
|
+
const expandedLlmPrunedIds = expandBatchIds(llmPrunedIds);
|
|
261
215
|
const finalNewlyPrunedIds = Array.from(expandedPrunedIds).filter(id => !alreadyPrunedIds.includes(id));
|
|
262
|
-
// finalPrunedIds includes everything (new + already pruned) for logging
|
|
263
216
|
const finalPrunedIds = Array.from(expandedPrunedIds);
|
|
264
|
-
// ============================================================
|
|
265
217
|
// PHASE 4: CALCULATE STATS & NOTIFICATION
|
|
266
|
-
// ============================================================
|
|
267
|
-
// Calculate token savings once (used by both notification and log)
|
|
268
218
|
const tokensSaved = await this.calculateTokensSaved(finalNewlyPrunedIds, toolOutputs);
|
|
269
|
-
// Accumulate session stats (for showing cumulative totals in UI)
|
|
270
219
|
const currentStats = this.statsState.get(sessionID) ?? { totalToolsPruned: 0, totalTokensSaved: 0 };
|
|
271
220
|
const sessionStats = {
|
|
272
221
|
totalToolsPruned: currentStats.totalToolsPruned + finalNewlyPrunedIds.length,
|
|
273
222
|
totalTokensSaved: currentStats.totalTokensSaved + tokensSaved
|
|
274
223
|
};
|
|
275
224
|
this.statsState.set(sessionID, sessionStats);
|
|
276
|
-
// Determine notification mode based on which strategies ran
|
|
277
225
|
const hasLlmAnalysis = strategies.includes('ai-analysis');
|
|
278
226
|
if (hasLlmAnalysis) {
|
|
279
|
-
await this.sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails,
|
|
227
|
+
await this.sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, expandedLlmPrunedIds, toolMetadata, tokensSaved, sessionStats);
|
|
280
228
|
}
|
|
281
229
|
else {
|
|
282
230
|
await this.sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails, tokensSaved, sessionStats);
|
|
283
231
|
}
|
|
284
|
-
// ============================================================
|
|
285
232
|
// PHASE 5: STATE UPDATE
|
|
286
|
-
// ============================================================
|
|
287
|
-
// Merge newly pruned IDs with existing ones (using expanded IDs)
|
|
288
233
|
const allPrunedIds = [...new Set([...alreadyPrunedIds, ...finalPrunedIds])];
|
|
289
234
|
this.prunedIdsState.set(sessionID, allPrunedIds);
|
|
290
|
-
// Log final summary
|
|
291
|
-
// Format: "Pruned 5/5 tools (~4.2K tokens), 0 kept" or with breakdown if both duplicate and llm
|
|
292
235
|
const prunedCount = finalNewlyPrunedIds.length;
|
|
293
236
|
const keptCount = candidateCount - prunedCount;
|
|
294
237
|
const hasBoth = deduplicatedIds.length > 0 && llmPrunedIds.length > 0;
|
|
295
238
|
const breakdown = hasBoth ? ` (${deduplicatedIds.length} duplicate, ${llmPrunedIds.length} llm)` : "";
|
|
296
|
-
// Build log metadata
|
|
297
239
|
const logMeta = { trigger: options.trigger };
|
|
298
240
|
if (options.reason) {
|
|
299
241
|
logMeta.reason = options.reason;
|
|
@@ -303,7 +245,7 @@ export class Janitor {
|
|
|
303
245
|
prunedCount: finalNewlyPrunedIds.length,
|
|
304
246
|
tokensSaved,
|
|
305
247
|
deduplicatedIds,
|
|
306
|
-
llmPrunedIds,
|
|
248
|
+
llmPrunedIds: expandedLlmPrunedIds,
|
|
307
249
|
deduplicationDetails,
|
|
308
250
|
toolMetadata,
|
|
309
251
|
sessionStats
|
|
@@ -314,17 +256,10 @@ export class Janitor {
|
|
|
314
256
|
error: error.message,
|
|
315
257
|
trigger: options.trigger
|
|
316
258
|
});
|
|
317
|
-
// Don't throw - this is a fire-and-forget background process
|
|
318
|
-
// Silently fail and try again on next idle event
|
|
319
259
|
return null;
|
|
320
260
|
}
|
|
321
261
|
}
|
|
322
|
-
/**
|
|
323
|
-
* Helper function to shorten paths for display
|
|
324
|
-
*/
|
|
325
262
|
shortenPath(input) {
|
|
326
|
-
// Handle compound strings like: "pattern" in /absolute/path
|
|
327
|
-
// Extract and shorten just the path portion
|
|
328
263
|
const inPathMatch = input.match(/^(.+) in (.+)$/);
|
|
329
264
|
if (inPathMatch) {
|
|
330
265
|
const prefix = inPathMatch[1];
|
|
@@ -334,31 +269,23 @@ export class Janitor {
|
|
|
334
269
|
}
|
|
335
270
|
return this.shortenSinglePath(input);
|
|
336
271
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Shorten a single path string
|
|
339
|
-
*/
|
|
340
272
|
shortenSinglePath(path) {
|
|
341
273
|
const homeDir = require('os').homedir();
|
|
342
|
-
// Strip working directory FIRST (before ~ replacement) for cleaner relative paths
|
|
343
274
|
if (this.workingDirectory) {
|
|
344
275
|
if (path.startsWith(this.workingDirectory + '/')) {
|
|
345
276
|
return path.slice(this.workingDirectory.length + 1);
|
|
346
277
|
}
|
|
347
|
-
// Exact match (the directory itself)
|
|
348
278
|
if (path === this.workingDirectory) {
|
|
349
279
|
return '.';
|
|
350
280
|
}
|
|
351
281
|
}
|
|
352
|
-
// Replace home directory with ~
|
|
353
282
|
if (path.startsWith(homeDir)) {
|
|
354
283
|
path = '~' + path.slice(homeDir.length);
|
|
355
284
|
}
|
|
356
|
-
// Shorten node_modules paths: show package + file only
|
|
357
285
|
const nodeModulesMatch = path.match(/node_modules\/(@[^\/]+\/[^\/]+|[^\/]+)\/(.*)/);
|
|
358
286
|
if (nodeModulesMatch) {
|
|
359
287
|
return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}`;
|
|
360
288
|
}
|
|
361
|
-
// Try matching against ~ version of working directory (for paths already with ~)
|
|
362
289
|
if (this.workingDirectory) {
|
|
363
290
|
const workingDirWithTilde = this.workingDirectory.startsWith(homeDir)
|
|
364
291
|
? '~' + this.workingDirectory.slice(homeDir.length)
|
|
@@ -372,11 +299,6 @@ export class Janitor {
|
|
|
372
299
|
}
|
|
373
300
|
return path;
|
|
374
301
|
}
|
|
375
|
-
/**
|
|
376
|
-
* Replace pruned tool outputs with placeholder text to save tokens in janitor context
|
|
377
|
-
* This applies the same replacement logic as the global fetch wrapper, but for the
|
|
378
|
-
* janitor's shadow inference to avoid sending already-pruned content to the LLM
|
|
379
|
-
*/
|
|
380
302
|
replacePrunedToolOutputs(messages, prunedIds) {
|
|
381
303
|
if (prunedIds.length === 0)
|
|
382
304
|
return messages;
|
|
@@ -391,7 +313,6 @@ export class Janitor {
|
|
|
391
313
|
part.callID &&
|
|
392
314
|
prunedIdsSet.has(part.callID.toLowerCase()) &&
|
|
393
315
|
part.state?.output) {
|
|
394
|
-
// Replace with the same placeholder used by the global fetch wrapper
|
|
395
316
|
return {
|
|
396
317
|
...part,
|
|
397
318
|
state: {
|
|
@@ -405,9 +326,6 @@ export class Janitor {
|
|
|
405
326
|
};
|
|
406
327
|
});
|
|
407
328
|
}
|
|
408
|
-
/**
|
|
409
|
-
* Helper function to calculate token savings from tool outputs
|
|
410
|
-
*/
|
|
411
329
|
async calculateTokensSaved(prunedIds, toolOutputs) {
|
|
412
330
|
const outputsToTokenize = [];
|
|
413
331
|
for (const prunedId of prunedIds) {
|
|
@@ -417,59 +335,46 @@ export class Janitor {
|
|
|
417
335
|
}
|
|
418
336
|
}
|
|
419
337
|
if (outputsToTokenize.length > 0) {
|
|
420
|
-
// Use batch tokenization for efficiency (lazy loads gpt-tokenizer)
|
|
421
338
|
const tokenCounts = await estimateTokensBatch(outputsToTokenize);
|
|
422
339
|
return tokenCounts.reduce((sum, count) => sum + count, 0);
|
|
423
340
|
}
|
|
424
341
|
return 0;
|
|
425
342
|
}
|
|
426
|
-
/**
|
|
427
|
-
* Build a summary of tools by grouping them
|
|
428
|
-
* Uses shared extractParameterKey logic for consistent parameter extraction
|
|
429
|
-
*
|
|
430
|
-
* Note: prunedIds may be in original case (from LLM) but toolMetadata uses lowercase keys
|
|
431
|
-
*/
|
|
432
343
|
buildToolsSummary(prunedIds, toolMetadata) {
|
|
433
344
|
const toolsSummary = new Map();
|
|
434
|
-
// Helper function to truncate long strings
|
|
435
345
|
const truncate = (str, maxLen = 60) => {
|
|
436
346
|
if (str.length <= maxLen)
|
|
437
347
|
return str;
|
|
438
348
|
return str.slice(0, maxLen - 3) + '...';
|
|
439
349
|
};
|
|
440
350
|
for (const prunedId of prunedIds) {
|
|
441
|
-
// Normalize ID to lowercase for lookup (toolMetadata uses lowercase keys)
|
|
442
351
|
const normalizedId = prunedId.toLowerCase();
|
|
443
352
|
const metadata = toolMetadata.get(normalizedId);
|
|
444
353
|
if (metadata) {
|
|
445
354
|
const toolName = metadata.tool;
|
|
355
|
+
if (toolName === 'batch')
|
|
356
|
+
continue;
|
|
446
357
|
if (!toolsSummary.has(toolName)) {
|
|
447
358
|
toolsSummary.set(toolName, []);
|
|
448
359
|
}
|
|
449
|
-
// Use shared parameter extraction logic
|
|
450
360
|
const paramKey = extractParameterKey(metadata);
|
|
451
361
|
if (paramKey) {
|
|
452
|
-
// Apply path shortening and truncation for display
|
|
453
362
|
const displayKey = truncate(this.shortenPath(paramKey), 80);
|
|
454
363
|
toolsSummary.get(toolName).push(displayKey);
|
|
455
364
|
}
|
|
456
365
|
else {
|
|
457
|
-
// For tools with no extractable parameter key, add a placeholder
|
|
458
|
-
// This ensures the tool still shows up in the summary
|
|
459
366
|
toolsSummary.get(toolName).push('(default)');
|
|
460
367
|
}
|
|
461
368
|
}
|
|
462
369
|
}
|
|
463
370
|
return toolsSummary;
|
|
464
371
|
}
|
|
465
|
-
/**
|
|
466
|
-
* Group deduplication details by tool type
|
|
467
|
-
* Shared helper used by notifications and tool output formatting
|
|
468
|
-
*/
|
|
469
372
|
groupDeduplicationDetails(deduplicationDetails) {
|
|
470
373
|
const grouped = new Map();
|
|
471
374
|
for (const [_, details] of deduplicationDetails) {
|
|
472
375
|
const { toolName, parameterKey, duplicateCount } = details;
|
|
376
|
+
if (toolName === 'batch')
|
|
377
|
+
continue;
|
|
473
378
|
if (!grouped.has(toolName)) {
|
|
474
379
|
grouped.set(toolName, []);
|
|
475
380
|
}
|
|
@@ -480,10 +385,6 @@ export class Janitor {
|
|
|
480
385
|
}
|
|
481
386
|
return grouped;
|
|
482
387
|
}
|
|
483
|
-
/**
|
|
484
|
-
* Format grouped deduplication results as lines
|
|
485
|
-
* Shared helper for building deduplication summaries
|
|
486
|
-
*/
|
|
487
388
|
formatDeduplicationLines(grouped, indent = ' ') {
|
|
488
389
|
const lines = [];
|
|
489
390
|
for (const [toolName, items] of grouped.entries()) {
|
|
@@ -494,10 +395,6 @@ export class Janitor {
|
|
|
494
395
|
}
|
|
495
396
|
return lines;
|
|
496
397
|
}
|
|
497
|
-
/**
|
|
498
|
-
* Format tool summary (from buildToolsSummary) as lines
|
|
499
|
-
* Shared helper for building LLM-pruned summaries
|
|
500
|
-
*/
|
|
501
398
|
formatToolSummaryLines(toolsSummary, indent = ' ') {
|
|
502
399
|
const lines = [];
|
|
503
400
|
for (const [toolName, params] of toolsSummary.entries()) {
|
|
@@ -513,47 +410,34 @@ export class Janitor {
|
|
|
513
410
|
}
|
|
514
411
|
return lines;
|
|
515
412
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Send minimal summary notification (just tokens saved and count)
|
|
518
|
-
*/
|
|
519
413
|
async sendMinimalNotification(sessionID, totalPruned, tokensSaved, sessionStats) {
|
|
520
414
|
if (totalPruned === 0)
|
|
521
415
|
return;
|
|
522
416
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
523
417
|
const toolText = totalPruned === 1 ? 'tool' : 'tools';
|
|
524
418
|
let message = `๐งน DCP: Saved ~${tokensFormatted} tokens (${totalPruned} ${toolText} pruned)`;
|
|
525
|
-
// Add session totals if there's been more than one pruning run
|
|
526
419
|
if (sessionStats.totalToolsPruned > totalPruned) {
|
|
527
420
|
message += ` โ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
528
421
|
}
|
|
529
422
|
await this.sendIgnoredMessage(sessionID, message);
|
|
530
423
|
}
|
|
531
|
-
/**
|
|
532
|
-
* Auto mode notification - shows only deduplication results
|
|
533
|
-
*/
|
|
534
424
|
async sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails, tokensSaved, sessionStats) {
|
|
535
425
|
if (deduplicatedIds.length === 0)
|
|
536
426
|
return;
|
|
537
|
-
// Check if notifications are disabled
|
|
538
427
|
if (this.pruningSummary === 'off')
|
|
539
428
|
return;
|
|
540
|
-
// Send minimal notification if configured
|
|
541
429
|
if (this.pruningSummary === 'minimal') {
|
|
542
430
|
await this.sendMinimalNotification(sessionID, deduplicatedIds.length, tokensSaved, sessionStats);
|
|
543
431
|
return;
|
|
544
432
|
}
|
|
545
|
-
// Otherwise send detailed notification
|
|
546
433
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
547
434
|
const toolText = deduplicatedIds.length === 1 ? 'tool' : 'tools';
|
|
548
435
|
let message = `๐งน DCP: Saved ~${tokensFormatted} tokens (${deduplicatedIds.length} duplicate ${toolText} removed)`;
|
|
549
|
-
// Add session totals if there's been more than one pruning run
|
|
550
436
|
if (sessionStats.totalToolsPruned > deduplicatedIds.length) {
|
|
551
437
|
message += ` โ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
552
438
|
}
|
|
553
439
|
message += '\n';
|
|
554
|
-
// Group by tool type using shared helper
|
|
555
440
|
const grouped = this.groupDeduplicationDetails(deduplicationDetails);
|
|
556
|
-
// Display grouped results (with UI-specific formatting: total dupes header, limit to 5)
|
|
557
441
|
for (const [toolName, items] of grouped.entries()) {
|
|
558
442
|
const totalDupes = items.reduce((sum, item) => sum + (item.count - 1), 0);
|
|
559
443
|
message += `\n${toolName} (${totalDupes} duplicate${totalDupes > 1 ? 's' : ''}):\n`;
|
|
@@ -567,22 +451,16 @@ export class Janitor {
|
|
|
567
451
|
}
|
|
568
452
|
await this.sendIgnoredMessage(sessionID, message.trim());
|
|
569
453
|
}
|
|
570
|
-
/**
|
|
571
|
-
* Format pruning result for tool output (returned to AI)
|
|
572
|
-
* Uses shared helpers for consistency with UI notifications
|
|
573
|
-
*/
|
|
574
454
|
formatPruningResultForTool(result) {
|
|
575
455
|
const lines = [];
|
|
576
456
|
lines.push(`Context pruning complete. Pruned ${result.prunedCount} tool outputs.`);
|
|
577
457
|
lines.push('');
|
|
578
|
-
// Section 1: Deduplicated tools
|
|
579
458
|
if (result.deduplicatedIds.length > 0 && result.deduplicationDetails.size > 0) {
|
|
580
459
|
lines.push(`Duplicates removed (${result.deduplicatedIds.length}):`);
|
|
581
460
|
const grouped = this.groupDeduplicationDetails(result.deduplicationDetails);
|
|
582
461
|
lines.push(...this.formatDeduplicationLines(grouped));
|
|
583
462
|
lines.push('');
|
|
584
463
|
}
|
|
585
|
-
// Section 2: LLM-pruned tools
|
|
586
464
|
if (result.llmPrunedIds.length > 0) {
|
|
587
465
|
lines.push(`Semantically pruned (${result.llmPrunedIds.length}):`);
|
|
588
466
|
const toolsSummary = this.buildToolsSummary(result.llmPrunedIds, result.toolMetadata);
|
|
@@ -590,42 +468,33 @@ export class Janitor {
|
|
|
590
468
|
}
|
|
591
469
|
return lines.join('\n').trim();
|
|
592
470
|
}
|
|
593
|
-
/**
|
|
594
|
-
* Smart mode notification - shows both deduplication and LLM analysis results
|
|
595
|
-
*/
|
|
596
471
|
async sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, llmPrunedIds, toolMetadata, tokensSaved, sessionStats) {
|
|
597
472
|
const totalPruned = deduplicatedIds.length + llmPrunedIds.length;
|
|
598
473
|
if (totalPruned === 0)
|
|
599
474
|
return;
|
|
600
|
-
// Check if notifications are disabled
|
|
601
475
|
if (this.pruningSummary === 'off')
|
|
602
476
|
return;
|
|
603
|
-
// Send minimal notification if configured
|
|
604
477
|
if (this.pruningSummary === 'minimal') {
|
|
605
478
|
await this.sendMinimalNotification(sessionID, totalPruned, tokensSaved, sessionStats);
|
|
606
479
|
return;
|
|
607
480
|
}
|
|
608
|
-
// Otherwise send detailed notification
|
|
609
481
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
610
482
|
let message = `๐งน DCP: Saved ~${tokensFormatted} tokens (${totalPruned} tool${totalPruned > 1 ? 's' : ''} pruned)`;
|
|
611
|
-
// Add session totals if there's been more than one pruning run
|
|
612
483
|
if (sessionStats.totalToolsPruned > totalPruned) {
|
|
613
484
|
message += ` โ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
614
485
|
}
|
|
615
486
|
message += '\n';
|
|
616
|
-
// Section 1: Deduplicated tools
|
|
617
487
|
if (deduplicatedIds.length > 0 && deduplicationDetails) {
|
|
618
488
|
message += `\n๐ฆ Duplicates removed (${deduplicatedIds.length}):\n`;
|
|
619
489
|
const grouped = this.groupDeduplicationDetails(deduplicationDetails);
|
|
620
490
|
for (const [toolName, items] of grouped.entries()) {
|
|
621
491
|
message += ` ${toolName}:\n`;
|
|
622
492
|
for (const item of items) {
|
|
623
|
-
const removedCount = item.count - 1;
|
|
493
|
+
const removedCount = item.count - 1;
|
|
624
494
|
message += ` ${item.key} (${removedCount}ร duplicate)\n`;
|
|
625
495
|
}
|
|
626
496
|
}
|
|
627
497
|
}
|
|
628
|
-
// Section 2: LLM-pruned tools
|
|
629
498
|
if (llmPrunedIds.length > 0) {
|
|
630
499
|
message += `\n๐ค LLM analysis (${llmPrunedIds.length}):\n`;
|
|
631
500
|
const toolsSummary = this.buildToolsSummary(llmPrunedIds, toolMetadata);
|
|
@@ -637,7 +506,6 @@ export class Janitor {
|
|
|
637
506
|
}
|
|
638
507
|
}
|
|
639
508
|
}
|
|
640
|
-
// Handle any tools that weren't found in metadata (edge case)
|
|
641
509
|
const foundToolNames = new Set(toolsSummary.keys());
|
|
642
510
|
const missingTools = llmPrunedIds.filter(id => {
|
|
643
511
|
const normalizedId = id.toLowerCase();
|