token-pilot 0.12.0 → 0.14.1

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.
Files changed (69) hide show
  1. package/.claude-plugin/hooks/hooks.json +9 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/CHANGELOG.md +30 -1
  5. package/README.md +28 -7
  6. package/dist/config/defaults.js +12 -0
  7. package/dist/core/architecture-fingerprint.d.ts +34 -0
  8. package/dist/core/architecture-fingerprint.js +127 -0
  9. package/dist/core/budget-planner.d.ts +21 -0
  10. package/dist/core/budget-planner.js +68 -0
  11. package/dist/core/confidence.d.ts +31 -0
  12. package/dist/core/confidence.js +99 -0
  13. package/dist/core/context-registry.d.ts +14 -0
  14. package/dist/core/context-registry.js +55 -0
  15. package/dist/core/decision-trace.d.ts +31 -0
  16. package/dist/core/decision-trace.js +45 -0
  17. package/dist/core/intent-classifier.d.ts +13 -0
  18. package/dist/core/intent-classifier.js +44 -0
  19. package/dist/core/policy-engine.d.ts +41 -0
  20. package/dist/core/policy-engine.js +76 -0
  21. package/dist/core/session-analytics.d.ts +8 -0
  22. package/dist/core/session-analytics.js +86 -7
  23. package/dist/core/session-cache.d.ts +74 -0
  24. package/dist/core/session-cache.js +162 -0
  25. package/dist/core/validation.d.ts +3 -0
  26. package/dist/core/validation.js +3 -0
  27. package/dist/git/file-watcher.d.ts +6 -0
  28. package/dist/git/file-watcher.js +18 -2
  29. package/dist/git/watcher.d.ts +3 -0
  30. package/dist/git/watcher.js +6 -0
  31. package/dist/handlers/code-audit.d.ts +7 -2
  32. package/dist/handlers/code-audit.js +19 -5
  33. package/dist/handlers/explore-area.d.ts +10 -0
  34. package/dist/handlers/explore-area.js +39 -13
  35. package/dist/handlers/find-unused.d.ts +3 -0
  36. package/dist/handlers/find-unused.js +3 -2
  37. package/dist/handlers/find-usages.d.ts +7 -0
  38. package/dist/handlers/find-usages.js +36 -5
  39. package/dist/handlers/module-info.d.ts +3 -0
  40. package/dist/handlers/module-info.js +22 -2
  41. package/dist/handlers/project-overview.d.ts +1 -1
  42. package/dist/handlers/project-overview.js +18 -2
  43. package/dist/handlers/read-for-edit.d.ts +3 -0
  44. package/dist/handlers/read-for-edit.js +185 -3
  45. package/dist/handlers/read-range.d.ts +1 -1
  46. package/dist/handlers/read-range.js +16 -1
  47. package/dist/handlers/read-symbol.d.ts +1 -1
  48. package/dist/handlers/read-symbol.js +26 -2
  49. package/dist/handlers/related-files.d.ts +11 -0
  50. package/dist/handlers/related-files.js +178 -42
  51. package/dist/handlers/smart-read-many.js +70 -16
  52. package/dist/handlers/smart-read.js +10 -1
  53. package/dist/handlers/test-summary.js +26 -3
  54. package/dist/hooks/installer.d.ts +12 -8
  55. package/dist/hooks/installer.js +24 -8
  56. package/dist/index.d.ts +16 -1
  57. package/dist/index.js +61 -55
  58. package/dist/server.js +395 -30
  59. package/dist/types.d.ts +12 -0
  60. package/package.json +5 -3
  61. package/start.sh +28 -27
  62. package/dist/handlers/class-hierarchy.d.ts +0 -11
  63. package/dist/handlers/class-hierarchy.js +0 -28
  64. package/dist/handlers/export-ast-index.d.ts +0 -22
  65. package/dist/handlers/export-ast-index.js +0 -175
  66. package/dist/handlers/find-implementations.d.ts +0 -11
  67. package/dist/handlers/find-implementations.js +0 -27
  68. package/dist/handlers/search-code.d.ts +0 -14
  69. package/dist/handlers/search-code.js +0 -32
@@ -47,6 +47,11 @@ export class ContextRegistry {
47
47
  return false;
48
48
  return entry.loaded.some(r => r.symbolName === symbolName);
49
49
  }
50
+ /** Check if any region of a file has been loaded into context. */
51
+ hasAnyLoaded(path) {
52
+ const entry = this.entries.get(path);
53
+ return !!entry && entry.loaded.length > 0;
54
+ }
50
55
  isStale(path, currentHash) {
51
56
  const entry = this.entries.get(path);
52
57
  if (!entry)
@@ -91,6 +96,56 @@ export class ContextRegistry {
91
96
  lines.push('HINT: File unchanged since last read. Use read_symbol() to reload specific parts, or read_diff() to see changes.');
92
97
  return lines.join('\n');
93
98
  }
99
+ /** Check if file was loaded in full (type='full' region exists). */
100
+ isFullyLoaded(path) {
101
+ const entry = this.entries.get(path);
102
+ if (!entry)
103
+ return false;
104
+ return entry.loaded.some(r => r.type === 'full');
105
+ }
106
+ /**
107
+ * Generate a compact dedup reminder for read_symbol.
108
+ * Fires when same symbol was already loaded OR full file is in context.
109
+ */
110
+ symbolReminder(path, symbolName) {
111
+ const entry = this.entries.get(path);
112
+ if (!entry)
113
+ return '';
114
+ const elapsed = formatDuration(Date.now() - entry.loadedAt);
115
+ const symbolRegion = entry.loaded.find(r => r.symbolName === symbolName);
116
+ const fullRegion = entry.loaded.find(r => r.type === 'full');
117
+ if (fullRegion) {
118
+ const loc = symbolRegion ? ` Symbol at [L${symbolRegion.startLine}-${symbolRegion.endLine}].` : '';
119
+ return [
120
+ `DEDUP: "${symbolName}" in ${path} — full file already in context (loaded ${elapsed} ago, ${fullRegion.tokens} tokens, unchanged).${loc}`,
121
+ 'HINT: File unchanged. No need to re-read. Use read_for_edit() if you need exact code for editing.',
122
+ ].join('\n');
123
+ }
124
+ if (symbolRegion) {
125
+ return [
126
+ `DEDUP: "${symbolName}" in ${path} — already loaded ${elapsed} ago [L${symbolRegion.startLine}-${symbolRegion.endLine}] (${symbolRegion.tokens} tokens, unchanged).`,
127
+ 'HINT: Symbol unchanged since last read. No need to re-read.',
128
+ ].join('\n');
129
+ }
130
+ return '';
131
+ }
132
+ /**
133
+ * Generate a compact dedup reminder for read_range.
134
+ * Only fires when full file is in context.
135
+ */
136
+ rangeReminder(path, startLine, endLine) {
137
+ const entry = this.entries.get(path);
138
+ if (!entry)
139
+ return '';
140
+ const fullRegion = entry.loaded.find(r => r.type === 'full');
141
+ if (!fullRegion)
142
+ return '';
143
+ const elapsed = formatDuration(Date.now() - entry.loadedAt);
144
+ return [
145
+ `DEDUP: ${path} [L${startLine}-${endLine}] — full file already in context (loaded ${elapsed} ago, ${fullRegion.tokens} tokens, unchanged).`,
146
+ 'HINT: File unchanged. No need to re-read. Use read_for_edit() if you need exact code for editing.',
147
+ ].join('\n');
148
+ }
94
149
  forget(path, symbolName) {
95
150
  if (symbolName) {
96
151
  const entry = this.entries.get(path);
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Decision trace — captures pre/post-execution metadata for analytics.
3
+ * Provides instrumentation data for budget planner advisory.
4
+ */
5
+ import type { ContextRegistry } from './context-registry.js';
6
+ import type { FileCache } from './file-cache.js';
7
+ export interface DecisionTrace {
8
+ fileSize?: number;
9
+ fileTotalLines?: number;
10
+ alreadyInContext: boolean;
11
+ estimatedCost: number;
12
+ actualCost: number;
13
+ cheaperAlternative?: string;
14
+ cheaperEstimate?: number;
15
+ }
16
+ export interface BuildTraceOptions {
17
+ absPath?: string;
18
+ tool: string;
19
+ args: Record<string, unknown>;
20
+ contextRegistry: ContextRegistry;
21
+ fileCache: FileCache;
22
+ tokensReturned: number;
23
+ tokensWouldBe: number;
24
+ recentlyEdited?: boolean;
25
+ }
26
+ /**
27
+ * Build a decision trace for a tool call.
28
+ * Gathers file metadata, context state, and budget planner advice.
29
+ */
30
+ export declare function buildDecisionTrace(opts: BuildTraceOptions): DecisionTrace;
31
+ //# sourceMappingURL=decision-trace.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Decision trace — captures pre/post-execution metadata for analytics.
3
+ * Provides instrumentation data for budget planner advisory.
4
+ */
5
+ import { suggestCheaperAlternative } from './budget-planner.js';
6
+ /**
7
+ * Build a decision trace for a tool call.
8
+ * Gathers file metadata, context state, and budget planner advice.
9
+ */
10
+ export function buildDecisionTrace(opts) {
11
+ const { absPath, tool, args, contextRegistry, fileCache, tokensReturned, tokensWouldBe } = opts;
12
+ let fileSize;
13
+ let fileTotalLines;
14
+ let alreadyInContext = false;
15
+ if (absPath) {
16
+ // Get file info from cache if available
17
+ const cached = fileCache.get(absPath);
18
+ if (cached) {
19
+ fileSize = cached.structure?.meta?.bytes;
20
+ fileTotalLines = cached.lines?.length ?? cached.structure?.meta?.lines;
21
+ }
22
+ alreadyInContext = contextRegistry.hasAnyLoaded(absPath);
23
+ }
24
+ const trace = {
25
+ fileSize,
26
+ fileTotalLines,
27
+ alreadyInContext,
28
+ estimatedCost: tokensWouldBe,
29
+ actualCost: tokensReturned,
30
+ };
31
+ // Budget planner advisory
32
+ const symbolKnown = !!(args.symbol || args.name);
33
+ const suggestion = suggestCheaperAlternative(tool, args, {
34
+ fileLines: fileTotalLines,
35
+ alreadyInContext,
36
+ symbolKnown,
37
+ recentlyEdited: opts.recentlyEdited ?? false,
38
+ });
39
+ if (suggestion) {
40
+ trace.cheaperAlternative = suggestion.tool;
41
+ trace.cheaperEstimate = suggestion.estimatedTokens;
42
+ }
43
+ return trace;
44
+ }
45
+ //# sourceMappingURL=decision-trace.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Intent classifier — maps tool + args to a task intent category.
3
+ * Used for per-intent analytics: which workflows save most tokens?
4
+ */
5
+ export type Intent = 'edit' | 'debug' | 'explore' | 'review' | 'analyze' | 'search' | 'read';
6
+ /**
7
+ * Classify the intent of a tool call based on tool name and optional args.
8
+ * Returns a stable intent category for analytics grouping.
9
+ */
10
+ export declare function classifyIntent(tool: string, _args?: Record<string, unknown>): Intent;
11
+ /** Get all known intents for iteration in reports. */
12
+ export declare const ALL_INTENTS: readonly Intent[];
13
+ //# sourceMappingURL=intent-classifier.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Intent classifier — maps tool + args to a task intent category.
3
+ * Used for per-intent analytics: which workflows save most tokens?
4
+ */
5
+ const TOOL_INTENT_MAP = {
6
+ // Edit workflow
7
+ read_for_edit: 'edit',
8
+ // Review workflow
9
+ smart_diff: 'review',
10
+ smart_log: 'review',
11
+ read_diff: 'review',
12
+ // Explore workflow
13
+ project_overview: 'explore',
14
+ explore_area: 'explore',
15
+ outline: 'explore',
16
+ // Search workflow
17
+ find_usages: 'search',
18
+ related_files: 'search',
19
+ // Analyze workflow
20
+ code_audit: 'analyze',
21
+ find_unused: 'analyze',
22
+ module_info: 'analyze',
23
+ // Debug workflow
24
+ test_summary: 'debug',
25
+ // Read workflow (default for reading tools)
26
+ smart_read: 'read',
27
+ read_symbol: 'read',
28
+ read_range: 'read',
29
+ smart_read_many: 'read',
30
+ // Analytics (meta — classify as explore)
31
+ session_analytics: 'explore',
32
+ };
33
+ /**
34
+ * Classify the intent of a tool call based on tool name and optional args.
35
+ * Returns a stable intent category for analytics grouping.
36
+ */
37
+ export function classifyIntent(tool, _args) {
38
+ return TOOL_INTENT_MAP[tool] ?? 'read';
39
+ }
40
+ /** Get all known intents for iteration in reports. */
41
+ export const ALL_INTENTS = [
42
+ 'edit', 'debug', 'explore', 'review', 'analyze', 'search', 'read',
43
+ ];
44
+ //# sourceMappingURL=intent-classifier.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Policy engine — configurable team policies for consistent token savings.
3
+ * Phase 1: advisory-only warnings, no blocking.
4
+ * Track 10: Team Policy Mode
5
+ */
6
+ export interface PolicyConfig {
7
+ /** Advisory hints when an expensive tool is used where a cheaper alternative exists */
8
+ preferCheapReads: boolean;
9
+ /** Track if read_for_edit was called before edit (advisory) */
10
+ requireReadForEditBeforeEdit: boolean;
11
+ /** Always cache project overview in session cache */
12
+ cacheProjectOverview: boolean;
13
+ /** Warn after N full-file reads in a session */
14
+ maxFullFileReads: number;
15
+ /** Warn when a single response exceeds this token threshold */
16
+ warnOnLargeReads: boolean;
17
+ /** Token threshold for large read warning */
18
+ largeReadThreshold: number;
19
+ }
20
+ export declare const DEFAULT_POLICIES: PolicyConfig;
21
+ export interface PolicyCheckContext {
22
+ fullFileReadsCount: number;
23
+ tokensReturned: number;
24
+ readForEditCalled?: Set<string>;
25
+ editTargetPath?: string;
26
+ }
27
+ export interface PolicyAdvisory {
28
+ level: 'info' | 'warn';
29
+ message: string;
30
+ }
31
+ /**
32
+ * Check policy rules and return advisory messages.
33
+ * Returns null if no policy violation detected.
34
+ */
35
+ export declare function checkPolicy(policy: PolicyConfig, tool: string, context: PolicyCheckContext): PolicyAdvisory | null;
36
+ /**
37
+ * Count how many full-file reads a tool represents.
38
+ * Returns 1 for full-read tools, 0 for targeted tools.
39
+ */
40
+ export declare function isFullReadTool(tool: string): boolean;
41
+ //# sourceMappingURL=policy-engine.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Policy engine — configurable team policies for consistent token savings.
3
+ * Phase 1: advisory-only warnings, no blocking.
4
+ * Track 10: Team Policy Mode
5
+ */
6
+ export const DEFAULT_POLICIES = {
7
+ preferCheapReads: true,
8
+ requireReadForEditBeforeEdit: true,
9
+ cacheProjectOverview: true,
10
+ maxFullFileReads: 10,
11
+ warnOnLargeReads: true,
12
+ largeReadThreshold: 2000,
13
+ };
14
+ /** Full-file read tools that count toward maxFullFileReads */
15
+ const FULL_READ_TOOLS = new Set([
16
+ 'smart_read',
17
+ 'smart_read_many',
18
+ ]);
19
+ /** Tools that indicate a cheaper alternative may exist */
20
+ const EXPENSIVE_TOOLS = {
21
+ smart_read: 'Consider read_symbol() or read_range() for targeted reads',
22
+ smart_read_many: 'Consider reading files individually with read_symbol()',
23
+ };
24
+ /**
25
+ * Check policy rules and return advisory messages.
26
+ * Returns null if no policy violation detected.
27
+ */
28
+ export function checkPolicy(policy, tool, context) {
29
+ // 1. Max full-file reads exceeded
30
+ if (policy.maxFullFileReads > 0 &&
31
+ FULL_READ_TOOLS.has(tool) &&
32
+ context.fullFileReadsCount >= policy.maxFullFileReads) {
33
+ return {
34
+ level: 'warn',
35
+ message: `POLICY: ${context.fullFileReadsCount} full-file reads this session (limit: ${policy.maxFullFileReads}). Consider read_symbol() or read_range() for targeted access.`,
36
+ };
37
+ }
38
+ // 2. Large read warning
39
+ if (policy.warnOnLargeReads &&
40
+ context.tokensReturned > policy.largeReadThreshold) {
41
+ return {
42
+ level: 'info',
43
+ message: `POLICY: Large response (~${context.tokensReturned} tokens). Future reads on this file: use read_symbol() or read_range() for targeted access.`,
44
+ };
45
+ }
46
+ // 3. Prefer cheap reads advisory
47
+ if (policy.preferCheapReads && EXPENSIVE_TOOLS[tool]) {
48
+ // Only advise when token count is high enough to matter
49
+ if (context.tokensReturned > 500) {
50
+ return {
51
+ level: 'info',
52
+ message: `POLICY: ${EXPENSIVE_TOOLS[tool]}`,
53
+ };
54
+ }
55
+ }
56
+ // 4. Require read_for_edit before edit
57
+ if (policy.requireReadForEditBeforeEdit &&
58
+ tool === 'edit' &&
59
+ context.editTargetPath &&
60
+ context.readForEditCalled &&
61
+ !context.readForEditCalled.has(context.editTargetPath)) {
62
+ return {
63
+ level: 'info',
64
+ message: `POLICY: Consider using read_for_edit("${context.editTargetPath}") before editing to get precise edit context.`,
65
+ };
66
+ }
67
+ return null;
68
+ }
69
+ /**
70
+ * Count how many full-file reads a tool represents.
71
+ * Returns 1 for full-read tools, 0 for targeted tools.
72
+ */
73
+ export function isFullReadTool(tool) {
74
+ return FULL_READ_TOOLS.has(tool);
75
+ }
76
+ //# sourceMappingURL=policy-engine.js.map
@@ -1,4 +1,7 @@
1
1
  import type { ContextModeStatus } from '../integration/context-mode-detector.js';
2
+ import type { Intent } from './intent-classifier.js';
3
+ import type { DecisionTrace } from './decision-trace.js';
4
+ export type SavingsCategory = 'compression' | 'cache' | 'dedup' | 'none';
2
5
  export interface ToolCall {
3
6
  tool: string;
4
7
  path?: string;
@@ -6,7 +9,12 @@ export interface ToolCall {
6
9
  tokensWouldBe: number;
7
10
  timestamp: number;
8
11
  delegatedToContextMode?: boolean;
12
+ sessionCacheHit?: boolean;
13
+ savingsCategory?: SavingsCategory;
14
+ intent?: Intent;
15
+ decisionTrace?: DecisionTrace;
9
16
  }
17
+ export type { Intent, DecisionTrace };
10
18
  /**
11
19
  * Tracks token savings and tool usage across a session.
12
20
  * When context-mode is detected, includes unified reporting.
@@ -1,4 +1,5 @@
1
1
  import { formatDuration } from './format-duration.js';
2
+ import { ALL_INTENTS } from './intent-classifier.js';
2
3
  /**
3
4
  * Tracks token savings and tool usage across a session.
4
5
  * When context-mode is detected, includes unified reporting.
@@ -24,10 +25,11 @@ export class SessionAnalytics {
24
25
  // Group by tool
25
26
  const byTool = new Map();
26
27
  for (const c of this.calls) {
27
- const existing = byTool.get(c.tool) ?? { count: 0, tokens: 0, saved: 0 };
28
+ const existing = byTool.get(c.tool) ?? { count: 0, tokens: 0, saved: 0, wouldBe: 0 };
28
29
  existing.count++;
29
30
  existing.tokens += c.tokensReturned;
30
31
  existing.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
32
+ existing.wouldBe += c.tokensWouldBe;
31
33
  byTool.set(c.tool, existing);
32
34
  }
33
35
  const lines = [
@@ -39,8 +41,12 @@ export class SessionAnalytics {
39
41
  '',
40
42
  'By tool:',
41
43
  ];
42
- for (const [tool, stats] of byTool) {
43
- lines.push(` ${tool}: ${stats.count} calls, ~${stats.tokens} tokens returned, ~${stats.saved} saved`);
44
+ const sortedTools = Array.from(byTool.entries()).sort((a, b) => b[1].saved - a[1].saved);
45
+ for (const [tool, stats] of sortedTools) {
46
+ const reduction = stats.wouldBe > 0
47
+ ? Math.round((1 - stats.tokens / stats.wouldBe) * 100)
48
+ : 0;
49
+ lines.push(` ${tool}: ${stats.count} calls, ~${stats.tokens} tokens returned, ~${stats.saved} saved (${reduction}% reduction)`);
44
50
  }
45
51
  // Top files by savings
46
52
  const byFile = new Map();
@@ -60,11 +66,48 @@ export class SessionAnalytics {
60
66
  lines.push(` ${file}: ~${saved} tokens saved`);
61
67
  }
62
68
  }
63
- // Reminders served (compact reminders that avoided re-reads)
64
- const reminders = this.calls.filter(c => c.tokensWouldBe > 0 && c.tokensReturned < c.tokensWouldBe * 0.1);
65
- if (reminders.length > 0) {
69
+ const lowValueTools = sortedTools
70
+ .map(([tool, stats]) => ({
71
+ tool,
72
+ reduction: stats.wouldBe > 0 ? Math.round((1 - stats.tokens / stats.wouldBe) * 100) : 0,
73
+ count: stats.count,
74
+ }))
75
+ .filter((tool) => tool.reduction < 20);
76
+ if (lowValueTools.length > 0) {
66
77
  lines.push('');
67
- lines.push(`Compact reminders served: ${reminders.length} (avoided full re-reads)`);
78
+ lines.push('Needs improvement:');
79
+ for (const tool of lowValueTools.slice(0, 5)) {
80
+ lines.push(` ${tool.tool}: only ${tool.reduction}% reduction across ${tool.count} call${tool.count === 1 ? '' : 's'}`);
81
+ }
82
+ }
83
+ // Savings breakdown by category
84
+ const byCategory = { compression: 0, cache: 0, dedup: 0, none: 0 };
85
+ for (const c of this.calls) {
86
+ const cat = c.savingsCategory ?? 'none';
87
+ byCategory[cat] += Math.max(0, c.tokensWouldBe - c.tokensReturned);
88
+ }
89
+ if (totalWouldBe > totalReturned) {
90
+ lines.push('');
91
+ lines.push('Savings breakdown:');
92
+ if (byCategory.compression > 0)
93
+ lines.push(` Compression (AST/structured): ~${byCategory.compression} tokens`);
94
+ if (byCategory.cache > 0)
95
+ lines.push(` Cache hits (session cache): ~${byCategory.cache} tokens`);
96
+ if (byCategory.dedup > 0)
97
+ lines.push(` Dedup (already in context): ~${byCategory.dedup} tokens`);
98
+ }
99
+ // Session cache hits
100
+ const cacheHits = this.calls.filter(c => c.sessionCacheHit);
101
+ if (cacheHits.length > 0) {
102
+ const cacheTokensSaved = cacheHits.reduce((s, c) => s + Math.max(0, c.tokensWouldBe - c.tokensReturned), 0);
103
+ lines.push('');
104
+ lines.push(`Session cache: ${cacheHits.length} hits / ${this.calls.length} calls (${Math.round(cacheHits.length / this.calls.length * 100)}% hit rate, ~${cacheTokensSaved} tokens saved)`);
105
+ }
106
+ // Dedup reminders served
107
+ const dedupCalls = this.calls.filter(c => c.savingsCategory === 'dedup');
108
+ if (dedupCalls.length > 0) {
109
+ lines.push('');
110
+ lines.push(`Compact reminders/dedup: ${dedupCalls.length} calls (avoided full re-reads)`);
68
111
  }
69
112
  // Delegation stats
70
113
  const delegated = this.calls.filter(c => c.delegatedToContextMode);
@@ -72,6 +115,42 @@ export class SessionAnalytics {
72
115
  lines.push('');
73
116
  lines.push(`Delegated to context-mode: ${delegated.length} calls`);
74
117
  }
118
+ // Per-intent breakdown (Track 2)
119
+ const callsWithIntent = this.calls.filter(c => c.intent);
120
+ if (callsWithIntent.length > 0) {
121
+ const byIntent = new Map();
122
+ for (const c of callsWithIntent) {
123
+ const intent = c.intent;
124
+ const existing = byIntent.get(intent) ?? { count: 0, saved: 0 };
125
+ existing.count++;
126
+ existing.saved += Math.max(0, c.tokensWouldBe - c.tokensReturned);
127
+ byIntent.set(intent, existing);
128
+ }
129
+ lines.push('');
130
+ lines.push('Per-intent breakdown:');
131
+ for (const intent of ALL_INTENTS) {
132
+ const stats = byIntent.get(intent);
133
+ if (stats) {
134
+ lines.push(` ${intent}: ${stats.count} call${stats.count === 1 ? '' : 's'}, ~${stats.saved} tokens saved`);
135
+ }
136
+ }
137
+ }
138
+ // Decision insights (Track 0)
139
+ const tracedCalls = this.calls.filter(c => c.decisionTrace);
140
+ if (tracedCalls.length > 0) {
141
+ const alreadyInContextCount = tracedCalls.filter(c => c.decisionTrace.alreadyInContext).length;
142
+ const totalEstimated = tracedCalls.reduce((s, c) => s + c.decisionTrace.estimatedCost, 0);
143
+ const totalActual = tracedCalls.reduce((s, c) => s + c.decisionTrace.actualCost, 0);
144
+ const avgReduction = totalEstimated > 0 ? Math.round((1 - totalActual / totalEstimated) * 100) : 0;
145
+ const missedSavings = tracedCalls.filter(c => c.decisionTrace.cheaperAlternative).length;
146
+ lines.push('');
147
+ lines.push('Decision insights:');
148
+ lines.push(` Files already in context: ${alreadyInContextCount} of ${tracedCalls.length} calls (${Math.round(alreadyInContextCount / tracedCalls.length * 100)}%)`);
149
+ lines.push(` Avg cost reduction: ${avgReduction}% (estimated → actual)`);
150
+ if (missedSavings > 0) {
151
+ lines.push(` Missed savings opportunities: ${missedSavings} call${missedSavings === 1 ? '' : 's'} could have used cheaper tools`);
152
+ }
153
+ }
75
154
  // Context-mode companion status
76
155
  if (this.contextModeStatus.detected) {
77
156
  lines.push('');
@@ -0,0 +1,74 @@
1
+ export interface SessionCacheEntry {
2
+ /** Cached handler result */
3
+ result: {
4
+ content: Array<{
5
+ type: string;
6
+ text: string;
7
+ }>;
8
+ [key: string]: unknown;
9
+ };
10
+ /** Absolute file paths (or dir prefixes ending with '/') this result depends on */
11
+ fileDeps: Set<string>;
12
+ /** Invalidate when AST index rebuilds */
13
+ dependsOnAst: boolean;
14
+ /** Invalidate when git state changes (branch switch, new commits) */
15
+ dependsOnGit: boolean;
16
+ /** When this entry was cached */
17
+ cachedAt: number;
18
+ /** Estimated token count of the result */
19
+ tokenEstimate: number;
20
+ /** Original tokensWouldBe from the first (non-cached) call — for honest cache savings */
21
+ tokensWouldBe?: number;
22
+ }
23
+ export interface SessionCacheDeps {
24
+ files?: string[];
25
+ dependsOnAst?: boolean;
26
+ dependsOnGit?: boolean;
27
+ }
28
+ export declare class SessionCache {
29
+ private maxEntries;
30
+ private entries;
31
+ /** Reverse index: file path → set of cache keys that depend on it */
32
+ private fileDepsIndex;
33
+ private hits;
34
+ private misses;
35
+ private invalidations;
36
+ constructor(maxEntries: number);
37
+ /**
38
+ * Generate deterministic cache key from tool name + args.
39
+ * Sorts args keys for consistency regardless of insertion order.
40
+ */
41
+ static makeCacheKey(tool: string, args: object): string;
42
+ /** Try to get a cached result. Returns null on miss. */
43
+ get(tool: string, args: object): SessionCacheEntry | null;
44
+ /** Store a result with its dependency metadata. */
45
+ set(tool: string, args: object, result: {
46
+ content: Array<{
47
+ type: string;
48
+ text: string;
49
+ }>;
50
+ [key: string]: unknown;
51
+ }, deps: SessionCacheDeps, tokenEstimate: number, tokensWouldBe?: number): void;
52
+ /**
53
+ * Invalidate all entries that depend on any of the given files.
54
+ * Checks both exact path match and directory prefix match.
55
+ */
56
+ invalidateByFiles(filePaths: string[]): number;
57
+ /** Invalidate all entries that depend on AST index state. */
58
+ invalidateByAst(): number;
59
+ /** Invalidate all entries that depend on git state. */
60
+ invalidateByGit(): number;
61
+ /** Clear all entries. */
62
+ invalidateAll(): void;
63
+ /** Cache statistics for analytics. */
64
+ stats(): {
65
+ entries: number;
66
+ hits: number;
67
+ misses: number;
68
+ hitRate: number;
69
+ invalidations: number;
70
+ };
71
+ private deleteEntry;
72
+ private evictOldest;
73
+ }
74
+ //# sourceMappingURL=session-cache.d.ts.map