@tarquinen/opencode-dcp 0.3.9 ā 0.3.11
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 +13 -95
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -280
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +0 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/janitor.d.ts +11 -3
- package/dist/lib/janitor.d.ts.map +1 -1
- package/dist/lib/janitor.js +106 -228
- package/dist/lib/janitor.js.map +1 -1
- package/dist/lib/logger.d.ts +5 -0
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +34 -17
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/model-selector.d.ts.map +1 -1
- package/dist/lib/model-selector.js +4 -71
- package/dist/lib/model-selector.js.map +1 -1
- package/dist/lib/tokenizer.d.ts +1 -3
- package/dist/lib/tokenizer.d.ts.map +1 -1
- package/dist/lib/tokenizer.js +4 -17
- package/dist/lib/tokenizer.js.map +1 -1
- package/package.json +1 -1
- package/dist/lib/state.d.ts +0 -6
- package/dist/lib/state.d.ts.map +0 -1
- package/dist/lib/state.js +0 -11
- package/dist/lib/state.js.map +0 -1
package/dist/lib/janitor.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Logger } from "./logger";
|
|
2
|
-
|
|
2
|
+
export interface SessionStats {
|
|
3
|
+
totalToolsPruned: number;
|
|
4
|
+
totalTokensSaved: number;
|
|
5
|
+
}
|
|
3
6
|
export declare class Janitor {
|
|
4
7
|
private client;
|
|
5
|
-
private
|
|
8
|
+
private prunedIdsState;
|
|
9
|
+
private statsState;
|
|
6
10
|
private logger;
|
|
7
11
|
private toolParametersCache;
|
|
8
12
|
private protectedTools;
|
|
@@ -12,7 +16,7 @@ export declare class Janitor {
|
|
|
12
16
|
private pruningMode;
|
|
13
17
|
private pruningSummary;
|
|
14
18
|
private workingDirectory?;
|
|
15
|
-
constructor(client: any,
|
|
19
|
+
constructor(client: any, prunedIdsState: Map<string, string[]>, statsState: Map<string, SessionStats>, logger: Logger, toolParametersCache: Map<string, any>, protectedTools: string[], modelCache: Map<string, {
|
|
16
20
|
providerID: string;
|
|
17
21
|
modelID: string;
|
|
18
22
|
}>, configModel?: string | undefined, // Format: "provider/model"
|
|
@@ -29,6 +33,10 @@ export declare class Janitor {
|
|
|
29
33
|
* Helper function to shorten paths for display
|
|
30
34
|
*/
|
|
31
35
|
private shortenPath;
|
|
36
|
+
/**
|
|
37
|
+
* Shorten a single path string
|
|
38
|
+
*/
|
|
39
|
+
private shortenSinglePath;
|
|
32
40
|
/**
|
|
33
41
|
* Replace pruned tool outputs with placeholder text to save tokens in janitor context
|
|
34
42
|
* This applies the same replacement logic as the global fetch wrapper, but for the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"janitor.d.ts","sourceRoot":"","sources":["../../lib/janitor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"janitor.d.ts","sourceRoot":"","sources":["../../lib/janitor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAMtC,MAAM,WAAW,YAAY;IACzB,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;CAC3B;AAED,qBAAa,OAAO;IAEZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,WAAW,CAAC;IACpB,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,gBAAgB,CAAC;gBAXjB,MAAM,EAAE,GAAG,EACX,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACrC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACrC,MAAM,EAAE,MAAM,EACd,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,cAAc,EAAE,MAAM,EAAE,EACxB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAChE,WAAW,CAAC,EAAE,MAAM,YAAA,EAAE,2BAA2B;IACjD,oBAAoB,GAAE,OAAc,EAAE,yCAAyC;IAC/E,WAAW,GAAE,MAAM,GAAG,OAAiB,EAAE,mBAAmB;IAC5D,cAAc,GAAE,KAAK,GAAG,SAAS,GAAG,UAAuB,EAAE,0BAA0B;IACvF,gBAAgB,CAAC,EAAE,MAAM,YAAA;IAGrC;;OAEG;YACW,kBAAkB;IAsB1B,GAAG,CAAC,SAAS,EAAE,MAAM;IAkR3B;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA0CzB;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IA8BhC;;OAEG;YACW,oBAAoB;IAmBlC;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAoCzB;;OAEG;YACW,uBAAuB;IAqBrC;;OAEG;YACW,wBAAwB;IA8DtC;;OAEG;YACW,yBAAyB;CA0F1C"}
|
package/dist/lib/janitor.js
CHANGED
|
@@ -5,7 +5,8 @@ import { estimateTokensBatch, formatTokenCount } from "./tokenizer";
|
|
|
5
5
|
import { detectDuplicates, extractParameterKey } from "./deduplicator";
|
|
6
6
|
export class Janitor {
|
|
7
7
|
client;
|
|
8
|
-
|
|
8
|
+
prunedIdsState;
|
|
9
|
+
statsState;
|
|
9
10
|
logger;
|
|
10
11
|
toolParametersCache;
|
|
11
12
|
protectedTools;
|
|
@@ -15,14 +16,15 @@ export class Janitor {
|
|
|
15
16
|
pruningMode;
|
|
16
17
|
pruningSummary;
|
|
17
18
|
workingDirectory;
|
|
18
|
-
constructor(client,
|
|
19
|
+
constructor(client, prunedIdsState, statsState, logger, toolParametersCache, protectedTools, modelCache, configModel, // Format: "provider/model"
|
|
19
20
|
showModelErrorToasts = true, // Whether to show toast for model errors
|
|
20
21
|
pruningMode = "smart", // Pruning strategy
|
|
21
22
|
pruningSummary = "detailed", // UI summary display mode
|
|
22
23
|
workingDirectory // Current working directory for relative path display
|
|
23
24
|
) {
|
|
24
25
|
this.client = client;
|
|
25
|
-
this.
|
|
26
|
+
this.prunedIdsState = prunedIdsState;
|
|
27
|
+
this.statsState = statsState;
|
|
26
28
|
this.logger = logger;
|
|
27
29
|
this.toolParametersCache = toolParametersCache;
|
|
28
30
|
this.protectedTools = protectedTools;
|
|
@@ -51,24 +53,16 @@ export class Janitor {
|
|
|
51
53
|
}]
|
|
52
54
|
}
|
|
53
55
|
});
|
|
54
|
-
this.logger.debug("janitor", "Sent ignored message to session", {
|
|
55
|
-
sessionID,
|
|
56
|
-
textLength: text.length
|
|
57
|
-
});
|
|
58
56
|
}
|
|
59
57
|
catch (error) {
|
|
60
|
-
this.logger.error("janitor", "Failed to send
|
|
61
|
-
sessionID,
|
|
58
|
+
this.logger.error("janitor", "Failed to send notification", {
|
|
62
59
|
error: error.message
|
|
63
60
|
});
|
|
64
|
-
// Don't fail the operation if sending the message fails
|
|
65
61
|
}
|
|
66
62
|
}
|
|
67
63
|
async run(sessionID) {
|
|
68
|
-
this.logger.info("janitor", "Starting analysis", { sessionID });
|
|
69
64
|
try {
|
|
70
65
|
// Fetch session info and messages from OpenCode API
|
|
71
|
-
this.logger.debug("janitor", "Fetching session info and messages", { sessionID });
|
|
72
66
|
const [sessionInfoResponse, messagesResponse] = await Promise.all([
|
|
73
67
|
this.client.session.get({ path: { id: sessionID } }),
|
|
74
68
|
this.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } })
|
|
@@ -76,16 +70,8 @@ export class Janitor {
|
|
|
76
70
|
const sessionInfo = sessionInfoResponse.data;
|
|
77
71
|
// Handle the response format - it should be { data: Array<{info, parts}> } or just the array
|
|
78
72
|
const messages = messagesResponse.data || messagesResponse;
|
|
79
|
-
this.logger.debug("janitor", "Retrieved messages", {
|
|
80
|
-
sessionID,
|
|
81
|
-
messageCount: messages.length
|
|
82
|
-
});
|
|
83
73
|
// If there are no messages or very few, skip analysis
|
|
84
74
|
if (!messages || messages.length < 3) {
|
|
85
|
-
this.logger.debug("janitor", "Too few messages to analyze, skipping", {
|
|
86
|
-
sessionID,
|
|
87
|
-
messageCount: messages?.length || 0
|
|
88
|
-
});
|
|
89
75
|
return;
|
|
90
76
|
}
|
|
91
77
|
// Extract tool call IDs from the session and track their output sizes
|
|
@@ -111,83 +97,32 @@ export class Janitor {
|
|
|
111
97
|
tool: part.tool,
|
|
112
98
|
parameters: parameters
|
|
113
99
|
});
|
|
114
|
-
// Debug: log what we're storing
|
|
115
|
-
if (normalizedId.startsWith('prt_') || part.tool === "read" || part.tool === "list") {
|
|
116
|
-
this.logger.debug("janitor", "Storing tool metadata", {
|
|
117
|
-
sessionID,
|
|
118
|
-
callID: normalizedId,
|
|
119
|
-
tool: part.tool,
|
|
120
|
-
hasParameters: !!parameters,
|
|
121
|
-
hasCached: !!cachedData,
|
|
122
|
-
parameters: parameters
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
100
|
// Track the output content for size calculation
|
|
126
101
|
if (part.state?.status === "completed" && part.state.output) {
|
|
127
102
|
toolOutputs.set(normalizedId, part.state.output);
|
|
128
103
|
}
|
|
129
104
|
// Check if this is a batch tool by looking at the tool name
|
|
130
105
|
if (part.tool === "batch") {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
batchToolChildren.set(batchId, []);
|
|
134
|
-
this.logger.debug("janitor", "Found batch tool", {
|
|
135
|
-
sessionID,
|
|
136
|
-
batchID: currentBatchId
|
|
137
|
-
});
|
|
106
|
+
currentBatchId = normalizedId;
|
|
107
|
+
batchToolChildren.set(normalizedId, []);
|
|
138
108
|
}
|
|
139
109
|
// If we're inside a batch and this is a prt_ (parallel) tool call, it's a child
|
|
140
110
|
else if (currentBatchId && normalizedId.startsWith('prt_')) {
|
|
141
|
-
|
|
142
|
-
children.push(normalizedId);
|
|
143
|
-
this.logger.debug("janitor", "Added child to batch tool", {
|
|
144
|
-
sessionID,
|
|
145
|
-
batchID: currentBatchId,
|
|
146
|
-
childID: normalizedId,
|
|
147
|
-
totalChildren: children.length
|
|
148
|
-
});
|
|
111
|
+
batchToolChildren.get(currentBatchId).push(normalizedId);
|
|
149
112
|
}
|
|
150
113
|
// If we hit a non-batch, non-prt_ tool, we're out of the batch
|
|
151
114
|
else if (currentBatchId && !normalizedId.startsWith('prt_')) {
|
|
152
|
-
this.logger.debug("janitor", "Batch tool ended", {
|
|
153
|
-
sessionID,
|
|
154
|
-
batchID: currentBatchId,
|
|
155
|
-
totalChildren: batchToolChildren.get(currentBatchId).length
|
|
156
|
-
});
|
|
157
115
|
currentBatchId = null;
|
|
158
116
|
}
|
|
159
117
|
}
|
|
160
118
|
}
|
|
161
119
|
}
|
|
162
120
|
}
|
|
163
|
-
// Log summary of batch tools found
|
|
164
|
-
if (batchToolChildren.size > 0) {
|
|
165
|
-
this.logger.debug("janitor", "Batch tool summary", {
|
|
166
|
-
sessionID,
|
|
167
|
-
batchCount: batchToolChildren.size,
|
|
168
|
-
batches: Array.from(batchToolChildren.entries()).map(([id, children]) => ({
|
|
169
|
-
batchID: id,
|
|
170
|
-
childCount: children.length,
|
|
171
|
-
childIDs: children
|
|
172
|
-
}))
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
121
|
// Get already pruned IDs to filter them out
|
|
176
|
-
const alreadyPrunedIds =
|
|
122
|
+
const alreadyPrunedIds = this.prunedIdsState.get(sessionID) ?? [];
|
|
177
123
|
const unprunedToolCallIds = toolCallIds.filter(id => !alreadyPrunedIds.includes(id));
|
|
178
|
-
this.logger.debug("janitor", "Found tool calls in session", {
|
|
179
|
-
sessionID,
|
|
180
|
-
toolCallCount: toolCallIds.length,
|
|
181
|
-
toolCallIds,
|
|
182
|
-
alreadyPrunedCount: alreadyPrunedIds.length,
|
|
183
|
-
alreadyPrunedIds: alreadyPrunedIds.slice(0, 5), // Show first 5 for brevity
|
|
184
|
-
unprunedCount: unprunedToolCallIds.length
|
|
185
|
-
});
|
|
186
124
|
// If there are no unpruned tool calls, skip analysis
|
|
187
125
|
if (unprunedToolCallIds.length === 0) {
|
|
188
|
-
this.logger.debug("janitor", "No unpruned tool calls found, skipping analysis", {
|
|
189
|
-
sessionID
|
|
190
|
-
});
|
|
191
126
|
return;
|
|
192
127
|
}
|
|
193
128
|
// ============================================================
|
|
@@ -196,11 +131,11 @@ export class Janitor {
|
|
|
196
131
|
const dedupeResult = detectDuplicates(toolMetadata, unprunedToolCallIds, this.protectedTools);
|
|
197
132
|
const deduplicatedIds = dedupeResult.duplicateIds;
|
|
198
133
|
const deduplicationDetails = dedupeResult.deduplicationDetails;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
134
|
+
// Calculate candidates available for pruning (excludes protected tools)
|
|
135
|
+
const candidateCount = unprunedToolCallIds.filter(id => {
|
|
136
|
+
const metadata = toolMetadata.get(id);
|
|
137
|
+
return !metadata || !this.protectedTools.includes(metadata.tool);
|
|
138
|
+
}).length;
|
|
204
139
|
// ============================================================
|
|
205
140
|
// PHASE 2: LLM ANALYSIS (only runs in "smart" mode)
|
|
206
141
|
// ============================================================
|
|
@@ -220,39 +155,15 @@ export class Janitor {
|
|
|
220
155
|
}
|
|
221
156
|
return true;
|
|
222
157
|
});
|
|
223
|
-
if (protectedToolCallIds.length > 0) {
|
|
224
|
-
this.logger.debug("janitor", "Protected tools excluded from pruning", {
|
|
225
|
-
sessionID,
|
|
226
|
-
protectedCount: protectedToolCallIds.length,
|
|
227
|
-
protectedTools: protectedToolCallIds.map(id => {
|
|
228
|
-
const metadata = toolMetadata.get(id);
|
|
229
|
-
return { id, tool: metadata?.tool };
|
|
230
|
-
})
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
158
|
// Run LLM analysis only if there are prunable tools
|
|
234
159
|
if (prunableToolCallIds.length > 0) {
|
|
235
|
-
this.logger.info("janitor", "Starting LLM analysis", {
|
|
236
|
-
sessionID,
|
|
237
|
-
candidateCount: prunableToolCallIds.length
|
|
238
|
-
});
|
|
239
160
|
// Select appropriate model with intelligent fallback
|
|
240
161
|
const cachedModelInfo = this.modelCache.get(sessionID);
|
|
241
162
|
const sessionModelInfo = extractModelFromSession(sessionInfo, this.logger);
|
|
242
163
|
const currentModelInfo = cachedModelInfo || sessionModelInfo;
|
|
243
|
-
if (cachedModelInfo) {
|
|
244
|
-
this.logger.debug("janitor", "Using cached model info", {
|
|
245
|
-
sessionID,
|
|
246
|
-
providerID: cachedModelInfo.providerID,
|
|
247
|
-
modelID: cachedModelInfo.modelID
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
164
|
const modelSelection = await selectModel(currentModelInfo, this.logger, this.configModel, this.workingDirectory);
|
|
251
|
-
this.logger.info("janitor",
|
|
252
|
-
|
|
253
|
-
modelInfo: modelSelection.modelInfo,
|
|
254
|
-
source: modelSelection.source,
|
|
255
|
-
reason: modelSelection.reason
|
|
165
|
+
this.logger.info("janitor", `Model: ${modelSelection.modelInfo.providerID}/${modelSelection.modelInfo.modelID}`, {
|
|
166
|
+
source: modelSelection.source
|
|
256
167
|
});
|
|
257
168
|
// Show toast if we had to fallback from a failed model
|
|
258
169
|
if (modelSelection.failedModel && this.showModelErrorToasts) {
|
|
@@ -265,53 +176,27 @@ export class Janitor {
|
|
|
265
176
|
duration: 5000
|
|
266
177
|
}
|
|
267
178
|
});
|
|
268
|
-
this.logger.info("janitor", "Toast notification shown for model fallback", {
|
|
269
|
-
failedModel: modelSelection.failedModel,
|
|
270
|
-
selectedModel: modelSelection.modelInfo
|
|
271
|
-
});
|
|
272
179
|
}
|
|
273
180
|
catch (toastError) {
|
|
274
|
-
this.logger.error("janitor", "Failed to show toast notification", {
|
|
275
|
-
error: toastError.message
|
|
276
|
-
});
|
|
277
181
|
// Don't fail the whole operation if toast fails
|
|
278
182
|
}
|
|
279
183
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
failedModel: modelSelection.failedModel,
|
|
283
|
-
selectedModel: modelSelection.modelInfo
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
// Log comprehensive stats before AI call
|
|
287
|
-
this.logger.info("janitor", "Preparing AI analysis", {
|
|
288
|
-
sessionID,
|
|
289
|
-
totalToolCallsInSession: toolCallIds.length,
|
|
290
|
-
alreadyPrunedCount: alreadyPrunedIds.length,
|
|
291
|
-
deduplicatedCount: deduplicatedIds.length,
|
|
292
|
-
protectedToolsCount: protectedToolCallIds.length,
|
|
293
|
-
candidatesForPruning: prunableToolCallIds.length,
|
|
294
|
-
candidateTools: prunableToolCallIds.map(id => {
|
|
295
|
-
const meta = toolMetadata.get(id);
|
|
296
|
-
return meta ? `${meta.tool}[${id.substring(0, 12)}...]` : id.substring(0, 12) + '...';
|
|
297
|
-
}).slice(0, 10), // Show first 10 for brevity
|
|
298
|
-
batchToolCount: batchToolChildren.size,
|
|
299
|
-
batchDetails: Array.from(batchToolChildren.entries()).map(([batchId, children]) => ({
|
|
300
|
-
batchId: batchId.substring(0, 20) + '...',
|
|
301
|
-
childCount: children.length
|
|
302
|
-
}))
|
|
303
|
-
});
|
|
304
|
-
this.logger.debug("janitor", "Starting shadow inference", { sessionID });
|
|
184
|
+
// Lazy import - only load the 2.8MB ai package when actually needed
|
|
185
|
+
const { generateObject } = await import('ai');
|
|
305
186
|
// Replace already-pruned tool outputs to save tokens in janitor context
|
|
306
187
|
const allPrunedSoFar = [...alreadyPrunedIds, ...deduplicatedIds];
|
|
307
188
|
const sanitizedMessages = this.replacePrunedToolOutputs(messages, allPrunedSoFar);
|
|
308
|
-
|
|
189
|
+
// Build the prompt for analysis
|
|
190
|
+
const analysisPrompt = buildAnalysisPrompt(prunableToolCallIds, sanitizedMessages, this.protectedTools, allPrunedSoFar, protectedToolCallIds);
|
|
191
|
+
// Save janitor shadow context directly (auth providers may bypass globalThis.fetch)
|
|
192
|
+
await this.logger.saveWrappedContext("janitor-shadow", [{ role: "user", content: analysisPrompt }], {
|
|
309
193
|
sessionID,
|
|
310
|
-
|
|
311
|
-
|
|
194
|
+
modelProvider: modelSelection.modelInfo.providerID,
|
|
195
|
+
modelID: modelSelection.modelInfo.modelID,
|
|
196
|
+
candidateToolCount: prunableToolCallIds.length,
|
|
197
|
+
alreadyPrunedCount: allPrunedSoFar.length,
|
|
198
|
+
protectedToolCount: protectedToolCallIds.length
|
|
312
199
|
});
|
|
313
|
-
// Lazy import - only load the 2.8MB ai package when actually needed
|
|
314
|
-
const { generateObject } = await import('ai');
|
|
315
200
|
// Analyze which tool calls are obsolete
|
|
316
201
|
const result = await generateObject({
|
|
317
202
|
model: modelSelection.model,
|
|
@@ -319,47 +204,23 @@ export class Janitor {
|
|
|
319
204
|
pruned_tool_call_ids: z.array(z.string()),
|
|
320
205
|
reasoning: z.string(),
|
|
321
206
|
}),
|
|
322
|
-
prompt:
|
|
207
|
+
prompt: analysisPrompt
|
|
323
208
|
});
|
|
324
209
|
// Filter LLM results to only include IDs that were actually candidates
|
|
325
210
|
// (LLM sometimes returns duplicate IDs that were already filtered out)
|
|
326
211
|
const rawLlmPrunedIds = result.object.pruned_tool_call_ids;
|
|
327
212
|
llmPrunedIds = rawLlmPrunedIds.filter(id => prunableToolCallIds.includes(id.toLowerCase()));
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
rawCount: rawLlmPrunedIds.length,
|
|
332
|
-
filteredCount: llmPrunedIds.length,
|
|
333
|
-
invalidIds: rawLlmPrunedIds.filter(id => !prunableToolCallIds.includes(id.toLowerCase()))
|
|
334
|
-
});
|
|
213
|
+
if (llmPrunedIds.length > 0) {
|
|
214
|
+
const reasoning = result.object.reasoning.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
215
|
+
this.logger.info("janitor", `LLM reasoning: ${reasoning.substring(0, 200)}${reasoning.length > 200 ? '...' : ''}`);
|
|
335
216
|
}
|
|
336
|
-
this.logger.info("janitor", "LLM analysis complete", {
|
|
337
|
-
sessionID,
|
|
338
|
-
llmPrunedCount: llmPrunedIds.length,
|
|
339
|
-
reasoning: result.object.reasoning
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
this.logger.info("janitor", "No prunable tools for LLM analysis", {
|
|
344
|
-
sessionID,
|
|
345
|
-
deduplicatedCount: deduplicatedIds.length,
|
|
346
|
-
protectedCount: protectedToolCallIds.length
|
|
347
|
-
});
|
|
348
217
|
}
|
|
349
218
|
}
|
|
350
|
-
else {
|
|
351
|
-
this.logger.info("janitor", "Skipping LLM analysis (auto mode)", {
|
|
352
|
-
sessionID,
|
|
353
|
-
deduplicatedCount: deduplicatedIds.length
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
// If mode is "auto", llmPrunedIds stays empty
|
|
357
219
|
// ============================================================
|
|
358
220
|
// PHASE 3: COMBINE & EXPAND
|
|
359
221
|
// ============================================================
|
|
360
222
|
const newlyPrunedIds = [...deduplicatedIds, ...llmPrunedIds];
|
|
361
223
|
if (newlyPrunedIds.length === 0) {
|
|
362
|
-
this.logger.info("janitor", "No tools to prune", { sessionID });
|
|
363
224
|
return;
|
|
364
225
|
}
|
|
365
226
|
// Expand batch tool IDs to include their children
|
|
@@ -370,12 +231,6 @@ export class Janitor {
|
|
|
370
231
|
// If this is a batch tool, add all its children
|
|
371
232
|
const children = batchToolChildren.get(normalizedId);
|
|
372
233
|
if (children) {
|
|
373
|
-
this.logger.debug("janitor", "Expanding batch tool to include children", {
|
|
374
|
-
sessionID,
|
|
375
|
-
batchID: normalizedId,
|
|
376
|
-
childCount: children.length,
|
|
377
|
-
childIDs: children
|
|
378
|
-
});
|
|
379
234
|
children.forEach(childId => expandedPrunedIds.add(childId));
|
|
380
235
|
}
|
|
381
236
|
}
|
|
@@ -383,48 +238,41 @@ export class Janitor {
|
|
|
383
238
|
const finalNewlyPrunedIds = Array.from(expandedPrunedIds).filter(id => !alreadyPrunedIds.includes(id));
|
|
384
239
|
// finalPrunedIds includes everything (new + already pruned) for logging
|
|
385
240
|
const finalPrunedIds = Array.from(expandedPrunedIds);
|
|
386
|
-
this.logger.info("janitor", "Analysis complete", {
|
|
387
|
-
sessionID,
|
|
388
|
-
prunedCount: finalPrunedIds.length,
|
|
389
|
-
deduplicatedCount: deduplicatedIds.length,
|
|
390
|
-
llmPrunedCount: llmPrunedIds.length,
|
|
391
|
-
prunedIds: finalPrunedIds
|
|
392
|
-
});
|
|
393
|
-
this.logger.debug("janitor", "Pruning ID details", {
|
|
394
|
-
sessionID,
|
|
395
|
-
alreadyPrunedCount: alreadyPrunedIds.length,
|
|
396
|
-
alreadyPrunedIds: alreadyPrunedIds,
|
|
397
|
-
finalPrunedCount: finalPrunedIds.length,
|
|
398
|
-
finalPrunedIds: finalPrunedIds,
|
|
399
|
-
newlyPrunedCount: finalNewlyPrunedIds.length,
|
|
400
|
-
newlyPrunedIds: finalNewlyPrunedIds
|
|
401
|
-
});
|
|
402
241
|
// ============================================================
|
|
403
242
|
// PHASE 4: NOTIFICATION
|
|
404
243
|
// ============================================================
|
|
244
|
+
// Calculate token savings once (used by both notification and log)
|
|
245
|
+
const tokensSaved = await this.calculateTokensSaved(finalNewlyPrunedIds, toolOutputs);
|
|
246
|
+
// Accumulate session stats (for showing cumulative totals in UI)
|
|
247
|
+
const currentStats = this.statsState.get(sessionID) ?? { totalToolsPruned: 0, totalTokensSaved: 0 };
|
|
248
|
+
const sessionStats = {
|
|
249
|
+
totalToolsPruned: currentStats.totalToolsPruned + finalNewlyPrunedIds.length,
|
|
250
|
+
totalTokensSaved: currentStats.totalTokensSaved + tokensSaved
|
|
251
|
+
};
|
|
252
|
+
this.statsState.set(sessionID, sessionStats);
|
|
405
253
|
if (this.pruningMode === "auto") {
|
|
406
|
-
await this.sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails,
|
|
254
|
+
await this.sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails, tokensSaved, sessionStats);
|
|
407
255
|
}
|
|
408
256
|
else {
|
|
409
|
-
await this.sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, llmPrunedIds, toolMetadata,
|
|
257
|
+
await this.sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, llmPrunedIds, toolMetadata, tokensSaved, sessionStats);
|
|
410
258
|
}
|
|
411
259
|
// ============================================================
|
|
412
260
|
// PHASE 5: STATE UPDATE
|
|
413
261
|
// ============================================================
|
|
414
262
|
// Merge newly pruned IDs with existing ones (using expanded IDs)
|
|
415
263
|
const allPrunedIds = [...new Set([...alreadyPrunedIds, ...finalPrunedIds])];
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
264
|
+
this.prunedIdsState.set(sessionID, allPrunedIds);
|
|
265
|
+
// Log final summary
|
|
266
|
+
// Format: "Pruned 5/5 tools (~4.2K tokens), 0 kept" or with breakdown if both duplicate and llm
|
|
267
|
+
const prunedCount = finalNewlyPrunedIds.length;
|
|
268
|
+
const keptCount = candidateCount - prunedCount;
|
|
269
|
+
const hasBoth = deduplicatedIds.length > 0 && llmPrunedIds.length > 0;
|
|
270
|
+
const breakdown = hasBoth ? ` (${deduplicatedIds.length} duplicate, ${llmPrunedIds.length} llm)` : "";
|
|
271
|
+
this.logger.info("janitor", `Pruned ${prunedCount}/${candidateCount} tools${breakdown}, ${keptCount} kept (~${formatTokenCount(tokensSaved)} tokens)`);
|
|
422
272
|
}
|
|
423
273
|
catch (error) {
|
|
424
274
|
this.logger.error("janitor", "Analysis failed", {
|
|
425
|
-
|
|
426
|
-
error: error.message,
|
|
427
|
-
stack: error.stack
|
|
275
|
+
error: error.message
|
|
428
276
|
});
|
|
429
277
|
// Don't throw - this is a fire-and-forget background process
|
|
430
278
|
// Silently fail and try again on next idle event
|
|
@@ -433,9 +281,34 @@ export class Janitor {
|
|
|
433
281
|
/**
|
|
434
282
|
* Helper function to shorten paths for display
|
|
435
283
|
*/
|
|
436
|
-
shortenPath(
|
|
437
|
-
//
|
|
284
|
+
shortenPath(input) {
|
|
285
|
+
// Handle compound strings like: "pattern" in /absolute/path
|
|
286
|
+
// Extract and shorten just the path portion
|
|
287
|
+
const inPathMatch = input.match(/^(.+) in (.+)$/);
|
|
288
|
+
if (inPathMatch) {
|
|
289
|
+
const prefix = inPathMatch[1];
|
|
290
|
+
const pathPart = inPathMatch[2];
|
|
291
|
+
const shortenedPath = this.shortenSinglePath(pathPart);
|
|
292
|
+
return `${prefix} in ${shortenedPath}`;
|
|
293
|
+
}
|
|
294
|
+
return this.shortenSinglePath(input);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Shorten a single path string
|
|
298
|
+
*/
|
|
299
|
+
shortenSinglePath(path) {
|
|
438
300
|
const homeDir = require('os').homedir();
|
|
301
|
+
// Strip working directory FIRST (before ~ replacement) for cleaner relative paths
|
|
302
|
+
if (this.workingDirectory) {
|
|
303
|
+
if (path.startsWith(this.workingDirectory + '/')) {
|
|
304
|
+
return path.slice(this.workingDirectory.length + 1);
|
|
305
|
+
}
|
|
306
|
+
// Exact match (the directory itself)
|
|
307
|
+
if (path === this.workingDirectory) {
|
|
308
|
+
return '.';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Replace home directory with ~
|
|
439
312
|
if (path.startsWith(homeDir)) {
|
|
440
313
|
path = '~' + path.slice(homeDir.length);
|
|
441
314
|
}
|
|
@@ -444,19 +317,17 @@ export class Janitor {
|
|
|
444
317
|
if (nodeModulesMatch) {
|
|
445
318
|
return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}`;
|
|
446
319
|
}
|
|
447
|
-
//
|
|
320
|
+
// Try matching against ~ version of working directory (for paths already with ~)
|
|
448
321
|
if (this.workingDirectory) {
|
|
449
|
-
// Try to match against the absolute working directory first
|
|
450
|
-
if (path.startsWith(this.workingDirectory + '/')) {
|
|
451
|
-
return path.slice(this.workingDirectory.length + 1);
|
|
452
|
-
}
|
|
453
|
-
// Also try matching against ~ version of working directory
|
|
454
322
|
const workingDirWithTilde = this.workingDirectory.startsWith(homeDir)
|
|
455
323
|
? '~' + this.workingDirectory.slice(homeDir.length)
|
|
456
324
|
: null;
|
|
457
325
|
if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) {
|
|
458
326
|
return path.slice(workingDirWithTilde.length + 1);
|
|
459
327
|
}
|
|
328
|
+
if (workingDirWithTilde && path === workingDirWithTilde) {
|
|
329
|
+
return '.';
|
|
330
|
+
}
|
|
460
331
|
}
|
|
461
332
|
return path;
|
|
462
333
|
}
|
|
@@ -506,7 +377,7 @@ export class Janitor {
|
|
|
506
377
|
}
|
|
507
378
|
if (outputsToTokenize.length > 0) {
|
|
508
379
|
// Use batch tokenization for efficiency (lazy loads gpt-tokenizer)
|
|
509
|
-
const tokenCounts = await estimateTokensBatch(outputsToTokenize
|
|
380
|
+
const tokenCounts = await estimateTokensBatch(outputsToTokenize);
|
|
510
381
|
return tokenCounts.reduce((sum, count) => sum + count, 0);
|
|
511
382
|
}
|
|
512
383
|
return 0;
|
|
@@ -553,19 +424,22 @@ export class Janitor {
|
|
|
553
424
|
/**
|
|
554
425
|
* Send minimal summary notification (just tokens saved and count)
|
|
555
426
|
*/
|
|
556
|
-
async sendMinimalNotification(sessionID, totalPruned,
|
|
427
|
+
async sendMinimalNotification(sessionID, totalPruned, tokensSaved, sessionStats) {
|
|
557
428
|
if (totalPruned === 0)
|
|
558
429
|
return;
|
|
559
|
-
const tokensSaved = await this.calculateTokensSaved(prunedIds, toolOutputs);
|
|
560
430
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
561
431
|
const toolText = totalPruned === 1 ? 'tool' : 'tools';
|
|
562
|
-
|
|
432
|
+
let message = `š§¹ DCP: Saved ~${tokensFormatted} tokens (${totalPruned} ${toolText} pruned)`;
|
|
433
|
+
// Add session totals if there's been more than one pruning run
|
|
434
|
+
if (sessionStats.totalToolsPruned > totalPruned) {
|
|
435
|
+
message += ` ā Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
436
|
+
}
|
|
563
437
|
await this.sendIgnoredMessage(sessionID, message);
|
|
564
438
|
}
|
|
565
439
|
/**
|
|
566
440
|
* Auto mode notification - shows only deduplication results
|
|
567
441
|
*/
|
|
568
|
-
async sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails,
|
|
442
|
+
async sendAutoModeNotification(sessionID, deduplicatedIds, deduplicationDetails, tokensSaved, sessionStats) {
|
|
569
443
|
if (deduplicatedIds.length === 0)
|
|
570
444
|
return;
|
|
571
445
|
// Check if notifications are disabled
|
|
@@ -573,15 +447,18 @@ export class Janitor {
|
|
|
573
447
|
return;
|
|
574
448
|
// Send minimal notification if configured
|
|
575
449
|
if (this.pruningSummary === 'minimal') {
|
|
576
|
-
await this.sendMinimalNotification(sessionID, deduplicatedIds.length,
|
|
450
|
+
await this.sendMinimalNotification(sessionID, deduplicatedIds.length, tokensSaved, sessionStats);
|
|
577
451
|
return;
|
|
578
452
|
}
|
|
579
453
|
// Otherwise send detailed notification
|
|
580
|
-
// Calculate token savings
|
|
581
|
-
const tokensSaved = await this.calculateTokensSaved(deduplicatedIds, toolOutputs);
|
|
582
454
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
583
455
|
const toolText = deduplicatedIds.length === 1 ? 'tool' : 'tools';
|
|
584
|
-
let message = `š§¹ DCP: Saved ~${tokensFormatted} tokens (${deduplicatedIds.length} duplicate ${toolText} removed)
|
|
456
|
+
let message = `š§¹ DCP: Saved ~${tokensFormatted} tokens (${deduplicatedIds.length} duplicate ${toolText} removed)`;
|
|
457
|
+
// Add session totals if there's been more than one pruning run
|
|
458
|
+
if (sessionStats.totalToolsPruned > deduplicatedIds.length) {
|
|
459
|
+
message += ` ā Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
460
|
+
}
|
|
461
|
+
message += '\n';
|
|
585
462
|
// Group by tool type
|
|
586
463
|
const grouped = new Map();
|
|
587
464
|
for (const [_, details] of deduplicationDetails) {
|
|
@@ -611,7 +488,7 @@ export class Janitor {
|
|
|
611
488
|
/**
|
|
612
489
|
* Smart mode notification - shows both deduplication and LLM analysis results
|
|
613
490
|
*/
|
|
614
|
-
async sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, llmPrunedIds, toolMetadata,
|
|
491
|
+
async sendSmartModeNotification(sessionID, deduplicatedIds, deduplicationDetails, llmPrunedIds, toolMetadata, tokensSaved, sessionStats) {
|
|
615
492
|
const totalPruned = deduplicatedIds.length + llmPrunedIds.length;
|
|
616
493
|
if (totalPruned === 0)
|
|
617
494
|
return;
|
|
@@ -620,16 +497,17 @@ export class Janitor {
|
|
|
620
497
|
return;
|
|
621
498
|
// Send minimal notification if configured
|
|
622
499
|
if (this.pruningSummary === 'minimal') {
|
|
623
|
-
|
|
624
|
-
await this.sendMinimalNotification(sessionID, totalPruned, toolOutputs, allPrunedIds);
|
|
500
|
+
await this.sendMinimalNotification(sessionID, totalPruned, tokensSaved, sessionStats);
|
|
625
501
|
return;
|
|
626
502
|
}
|
|
627
503
|
// Otherwise send detailed notification
|
|
628
|
-
// Calculate token savings
|
|
629
|
-
const allPrunedIds = [...deduplicatedIds, ...llmPrunedIds];
|
|
630
|
-
const tokensSaved = await this.calculateTokensSaved(allPrunedIds, toolOutputs);
|
|
631
504
|
const tokensFormatted = formatTokenCount(tokensSaved);
|
|
632
|
-
let message = `š§¹ DCP: Saved ~${tokensFormatted} tokens (${totalPruned} tool${totalPruned > 1 ? 's' : ''} pruned)
|
|
505
|
+
let message = `š§¹ DCP: Saved ~${tokensFormatted} tokens (${totalPruned} tool${totalPruned > 1 ? 's' : ''} pruned)`;
|
|
506
|
+
// Add session totals if there's been more than one pruning run
|
|
507
|
+
if (sessionStats.totalToolsPruned > totalPruned) {
|
|
508
|
+
message += ` ā Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`;
|
|
509
|
+
}
|
|
510
|
+
message += '\n';
|
|
633
511
|
// Section 1: Deduplicated tools
|
|
634
512
|
if (deduplicatedIds.length > 0 && deduplicationDetails) {
|
|
635
513
|
message += `\nš¦ Duplicates removed (${deduplicatedIds.length}):\n`;
|