Skip to content

Commit ccc36e2

Browse files
committed
release: v2.7.235 mcp working-set pressure telemetry
1 parent 2c2ab2a commit ccc36e2

File tree

7 files changed

+157
-12
lines changed

7 files changed

+157
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
All notable changes to this project will be documented in this file.
66

7+
## [2.7.235] — 2026-03-15
8+
9+
- feat(core/mcp): `packages/core/src/routers/mcpRouter.ts` now records working-set pressure snapshot fields (`loadedToolCount`, `hydratedSchemaCount`, configured caps, utilization percentages, idle-eviction threshold) on load/unload/hydrate telemetry events.
10+
- feat(core/mcp): `packages/core/src/mcp/toolSelectionTelemetry.ts` extends the telemetry contract with explicit working-set pressure/limit fields so dashboard consumers can reason about routing behavior under capacity pressure.
11+
- changed(web/mcp-search): `apps/web/src/app/dashboard/mcp/search/page.tsx` now surfaces pressure context in telemetry cards (loaded/hydrated utilization and idle-eviction threshold), sorts loaded-tool sections by recency, and highlights high idle-eviction risk in the working-set panel.
12+
- test(core/mcp): expanded `packages/core/src/services/metamcp-session-working-set.service.test.ts` for idle-threshold limit defaults/reconfiguration/clamping and stronger LRU-use assertions.
13+
- test(validation): reran focused core suites (`metamcp-session-working-set`, `CoreModelSelector`) with `23` tests passing; reran web `tsc --noEmit` with explicit `WEB_TSC_OK`.
14+
715
## [2.7.234] — 2026-03-15
816

917
- fix(web/session): `apps/web/src/app/dashboard/session/session-page-normalizers.ts` now preserves supervisor status `stopping` instead of collapsing it to `created`, so in-flight stop operations render truthfully in the dashboard.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.7.234
1+
2.7.235

VERSION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# Borg Project Version: 2.7.234
1+
# Borg Project Version: 2.7.235

apps/web/src/app/dashboard/mcp/search/page.tsx

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ type ToolSelectionTelemetryEvent = {
7474
autoLoadMinConfidence?: number;
7575
autoLoadExecutionStatus?: 'success' | 'error' | 'not-attempted';
7676
autoLoadExecutionError?: string;
77+
loadedToolCount?: number;
78+
hydratedSchemaCount?: number;
79+
maxLoadedTools?: number;
80+
maxHydratedSchemas?: number;
81+
idleEvictionThresholdMs?: number;
82+
loadedUtilizationPct?: number;
83+
hydratedUtilizationPct?: number;
7784
};
7885

7986
type WorkingSetEvictionEvent = {
@@ -194,6 +201,23 @@ function formatRelativeTimestamp(timestamp: number | null): string {
194201
return `${deltaHours}h ago`;
195202
}
196203

204+
function formatDurationCompact(durationMs: number): string {
205+
const clamped = Math.max(0, Math.round(durationMs));
206+
const seconds = Math.round(clamped / 1000);
207+
208+
if (seconds < 60) {
209+
return `${seconds}s`;
210+
}
211+
212+
const minutes = Math.round(seconds / 60);
213+
if (minutes < 60) {
214+
return `${minutes}m`;
215+
}
216+
217+
const hours = Math.round(minutes / 60);
218+
return `${hours}h`;
219+
}
220+
197221
export default function SearchDashboard() {
198222
const router = useRouter();
199223
const pathname = usePathname();
@@ -421,7 +445,14 @@ export default function SearchDashboard() {
421445
const alwaysOnWorkingSet = workingSet.filter((tool) => alwaysOnAdvertisedNames.has(tool.name));
422446
const keepWarmWorkingSet = workingSet.filter((tool) => alwaysLoadedTools.has(tool.name) && !alwaysOnAdvertisedNames.has(tool.name));
423447
const dynamicWorkingSet = workingSet.filter((tool) => !alwaysLoadedTools.has(tool.name) && !alwaysOnAdvertisedNames.has(tool.name));
448+
const sortedAlwaysOnWorkingSet = [...alwaysOnWorkingSet].sort((left, right) => left.lastAccessedAt - right.lastAccessedAt);
449+
const sortedKeepWarmWorkingSet = [...keepWarmWorkingSet].sort((left, right) => left.lastAccessedAt - right.lastAccessedAt);
450+
const sortedDynamicWorkingSet = [...dynamicWorkingSet].sort((left, right) => left.lastAccessedAt - right.lastAccessedAt);
424451
const hydratedCount = workingSet.filter((tool) => tool.hydrated).length;
452+
const idleEvictionThresholdMs = Math.max(
453+
0,
454+
((workingSetQuery.data?.limits as { idleEvictionThresholdMs?: number } | undefined)?.idleEvictionThresholdMs ?? 0),
455+
);
425456

426457
useEffect(() => {
427458
if (jsoncEditorQuery.data?.content && jsoncDraft.length === 0) {
@@ -1083,21 +1114,26 @@ export default function SearchDashboard() {
10831114
{workingSet.length > 0 ? (
10841115
<>
10851116
{[
1086-
{ label: 'Server always-on', tone: 'text-sky-300', tools: alwaysOnWorkingSet },
1087-
{ label: 'Keep warm profile', tone: 'text-cyan-300', tools: keepWarmWorkingSet },
1088-
{ label: 'Dynamic loaded', tone: 'text-zinc-300', tools: dynamicWorkingSet },
1117+
{ label: 'Server always-on', tone: 'text-sky-300', tools: sortedAlwaysOnWorkingSet },
1118+
{ label: 'Keep warm profile', tone: 'text-cyan-300', tools: sortedKeepWarmWorkingSet },
1119+
{ label: 'Dynamic loaded', tone: 'text-zinc-300', tools: sortedDynamicWorkingSet },
10891120
].map((section) => (
10901121
<div key={section.label} className="space-y-2">
10911122
<div className={`text-[10px] uppercase tracking-wider ${section.tone}`}>
10921123
{section.label} ({section.tools.length})
10931124
</div>
10941125
{section.tools.length > 0 ? section.tools.map((tool) => (
10951126
<div key={tool.name} className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 space-y-2">
1127+
{(() => {
1128+
const idleMs = tool.lastAccessedAt > 0 ? Math.max(0, Date.now() - tool.lastAccessedAt) : 0;
1129+
const nearingIdleEviction = idleEvictionThresholdMs > 0 && idleMs >= idleEvictionThresholdMs;
1130+
return (
1131+
<>
10961132
<div className="flex items-start justify-between gap-3">
10971133
<div className="min-w-0">
10981134
<div className="font-mono text-sm text-zinc-100 break-all">{tool.name}</div>
10991135
<div className="text-xs text-zinc-500 mt-1">
1100-
loaded {formatRelativeTimestamp(tool.lastLoadedAt)}
1136+
loaded {formatRelativeTimestamp(tool.lastLoadedAt)} • touched {formatRelativeTimestamp(tool.lastAccessedAt)} • idle {formatDurationCompact(idleMs)}
11011137
</div>
11021138
</div>
11031139
{tool.hydrated ? (
@@ -1110,6 +1146,11 @@ export default function SearchDashboard() {
11101146
</span>
11111147
)}
11121148
</div>
1149+
{nearingIdleEviction ? (
1150+
<div className="text-[10px] rounded border border-amber-500/30 bg-amber-500/10 px-2 py-1 text-amber-200 uppercase tracking-wider">
1151+
High eviction risk: idle beyond threshold ({formatDurationCompact(idleEvictionThresholdMs)})
1152+
</div>
1153+
) : null}
11131154
<div className="grid grid-cols-3 gap-2">
11141155
<Link
11151156
href={`/dashboard/mcp/inspector?tool=${encodeURIComponent(tool.name)}`}
@@ -1139,6 +1180,9 @@ export default function SearchDashboard() {
11391180
Unload
11401181
</Button>
11411182
</div>
1183+
</>
1184+
);
1185+
})()}
11421186
</div>
11431187
)) : (
11441188
<div className="rounded-lg border border-dashed border-zinc-800 p-3 text-xs text-zinc-500 text-center">
@@ -1735,6 +1779,23 @@ export default function SearchDashboard() {
17351779
{event.autoLoadSkipReason ? <div className="text-xs text-amber-300 break-all">auto-load skipped: {event.autoLoadSkipReason}</div> : null}
17361780
{event.autoLoadExecutionError ? <div className="text-xs text-red-300 break-all">auto-load failed: {event.autoLoadExecutionError}</div> : null}
17371781
{typeof event.latencyMs === 'number' ? <div className="text-xs text-zinc-500">latency: {event.latencyMs}ms</div> : null}
1782+
{typeof event.loadedToolCount === 'number' && typeof event.maxLoadedTools === 'number' ? (
1783+
<div className="text-xs text-zinc-500">
1784+
loaded working set: <span className="text-zinc-200">{event.loadedToolCount}/{event.maxLoadedTools}</span>
1785+
{typeof event.loadedUtilizationPct === 'number' ? ` (${event.loadedUtilizationPct}%)` : ''}
1786+
</div>
1787+
) : null}
1788+
{typeof event.hydratedSchemaCount === 'number' && typeof event.maxHydratedSchemas === 'number' ? (
1789+
<div className="text-xs text-zinc-500">
1790+
hydrated schemas: <span className="text-zinc-200">{event.hydratedSchemaCount}/{event.maxHydratedSchemas}</span>
1791+
{typeof event.hydratedUtilizationPct === 'number' ? ` (${event.hydratedUtilizationPct}%)` : ''}
1792+
</div>
1793+
) : null}
1794+
{typeof event.idleEvictionThresholdMs === 'number' ? (
1795+
<div className="text-xs text-zinc-500">
1796+
idle eviction threshold: <span className="text-zinc-200">{formatDurationCompact(event.idleEvictionThresholdMs)}</span>
1797+
</div>
1798+
) : null}
17381799
{event.source ? <div className="text-xs text-zinc-500">source: {event.source}</div> : null}
17391800
{event.evictedTools && event.evictedTools.length > 0 ? (
17401801
<div className="text-xs text-amber-300 break-all">evicted: {event.evictedTools.join(', ')}</div>

packages/core/src/mcp/toolSelectionTelemetry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export interface ToolSelectionTelemetryEvent {
3131
autoLoadMinConfidence?: number;
3232
autoLoadExecutionStatus?: 'success' | 'error' | 'not-attempted';
3333
autoLoadExecutionError?: string;
34+
loadedToolCount?: number;
35+
hydratedSchemaCount?: number;
36+
maxLoadedTools?: number;
37+
maxHydratedSchemas?: number;
38+
idleEvictionThresholdMs?: number;
39+
loadedUtilizationPct?: number;
40+
hydratedUtilizationPct?: number;
3441
}
3542

3643
const MAX_TELEMETRY_EVENTS = 100;

packages/core/src/routers/mcpRouter.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,50 @@ function parseEvictedToolsFromMessage(message: string): string[] {
5959
.filter(Boolean);
6060
}
6161

62+
type WorkingSetSnapshot = {
63+
limits?: {
64+
maxLoadedTools?: number;
65+
maxHydratedSchemas?: number;
66+
idleEvictionThresholdMs?: number;
67+
};
68+
tools?: Array<{ hydrated?: boolean }>;
69+
};
70+
71+
async function readWorkingSetSnapshot(
72+
server: NonNullable<ReturnType<typeof getMcpServer>>,
73+
): Promise<{
74+
loadedToolCount: number;
75+
hydratedSchemaCount: number;
76+
maxLoadedTools: number;
77+
maxHydratedSchemas: number;
78+
idleEvictionThresholdMs: number;
79+
loadedUtilizationPct: number;
80+
hydratedUtilizationPct: number;
81+
}> {
82+
const snapshot = parseToolJson<WorkingSetSnapshot>(
83+
await server.executeTool('list_loaded_tools', {}),
84+
{ limits: {}, tools: [] },
85+
);
86+
87+
const loadedToolCount = Array.isArray(snapshot.tools) ? snapshot.tools.length : 0;
88+
const hydratedSchemaCount = Array.isArray(snapshot.tools)
89+
? snapshot.tools.filter((tool) => Boolean(tool?.hydrated)).length
90+
: 0;
91+
const maxLoadedTools = Math.max(1, snapshot.limits?.maxLoadedTools ?? 0);
92+
const maxHydratedSchemas = Math.max(1, snapshot.limits?.maxHydratedSchemas ?? 0);
93+
const idleEvictionThresholdMs = Math.max(0, snapshot.limits?.idleEvictionThresholdMs ?? 0);
94+
95+
return {
96+
loadedToolCount,
97+
hydratedSchemaCount,
98+
maxLoadedTools,
99+
maxHydratedSchemas,
100+
idleEvictionThresholdMs,
101+
loadedUtilizationPct: Math.min(100, Math.round((loadedToolCount / maxLoadedTools) * 100)),
102+
hydratedUtilizationPct: Math.min(100, Math.round((hydratedSchemaCount / maxHydratedSchemas) * 100)),
103+
};
104+
}
105+
62106
function toSerializablePayload(value: unknown): unknown {
63107
try {
64108
return JSON.parse(JSON.stringify(value, (_key, current) => {
@@ -1011,13 +1055,15 @@ export const mcpRouter = t.router({
10111055
const result = await server.executeTool('load_tool', { name: input.name });
10121056
const message = getToolTextContent(result);
10131057
const evictedTools = parseEvictedToolsFromMessage(message);
1058+
const pressure = await readWorkingSetSnapshot(server);
10141059
toolSelectionTelemetry.record({
10151060
type: 'load',
10161061
toolName: input.name,
10171062
status: 'success',
10181063
message,
10191064
evictedTools,
10201065
latencyMs: toLatencyMs(startedAt),
1066+
...pressure,
10211067
});
10221068
return {
10231069
ok: true,
@@ -1036,12 +1082,14 @@ export const mcpRouter = t.router({
10361082
const startedAt = Date.now();
10371083
const result = await server.executeTool('unload_tool', { name: input.name });
10381084
const message = getToolTextContent(result);
1085+
const pressure = await readWorkingSetSnapshot(server);
10391086
toolSelectionTelemetry.record({
10401087
type: 'unload',
10411088
toolName: input.name,
10421089
status: 'success',
10431090
message,
10441091
latencyMs: toLatencyMs(startedAt),
1092+
...pressure,
10451093
});
10461094
return {
10471095
ok: true,
@@ -1063,13 +1111,15 @@ export const mcpRouter = t.router({
10631111
inputSchema: null,
10641112
evictedHydratedTools: [],
10651113
});
1114+
const pressure = await readWorkingSetSnapshot(server);
10661115
toolSelectionTelemetry.record({
10671116
type: 'hydrate',
10681117
toolName: input.name,
10691118
status: 'success',
10701119
message: 'schema hydrated',
10711120
evictedTools: Array.isArray(parsed.evictedHydratedTools) ? parsed.evictedHydratedTools : [],
10721121
latencyMs: toLatencyMs(startedAt),
1122+
...pressure,
10731123
});
10741124
return parsed;
10751125
}),

packages/core/src/services/metamcp-session-working-set.service.test.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { describe, expect, it, vi } from 'vitest';
22

33
import { SessionToolWorkingSet } from './metamcp-session-working-set.service.js';
44

@@ -9,6 +9,7 @@ describe('SessionToolWorkingSet', () => {
99
expect(workingSet.getLimits()).toEqual({
1010
maxLoadedTools: 16,
1111
maxHydratedSchemas: 8,
12+
idleEvictionThresholdMs: 5 * 60 * 1000,
1213
});
1314
});
1415

@@ -26,7 +27,7 @@ describe('SessionToolWorkingSet', () => {
2627
const evicted = workingSet.loadTool('delta');
2728

2829
expect(evicted).toEqual(['alpha']);
29-
expect(workingSet.getLoadedToolNames()).toEqual(['gamma', 'beta', 'delta']);
30+
expect(new Set(workingSet.getLoadedToolNames())).toEqual(new Set(['beta', 'gamma', 'delta']));
3031
});
3132

3233
it('evicts the least recently used hydrated schema independently of loaded metadata', () => {
@@ -68,6 +69,13 @@ describe('SessionToolWorkingSet', () => {
6869
});
6970

7071
it('refreshes LRU order when an already loaded tool is actually used', () => {
72+
const nowSpy = vi.spyOn(Date, 'now');
73+
let tick = 1_000;
74+
nowSpy.mockImplementation(() => {
75+
tick += 1;
76+
return tick;
77+
});
78+
7179
const workingSet = new SessionToolWorkingSet({
7280
maxLoadedTools: 3,
7381
maxHydratedSchemas: 2,
@@ -82,7 +90,13 @@ describe('SessionToolWorkingSet', () => {
8290
const evicted = workingSet.loadTool('delta');
8391

8492
expect(evicted).toEqual(['beta']);
85-
expect(workingSet.getLoadedToolNames()).toEqual(['gamma', 'alpha', 'delta']);
93+
expect(new Set(workingSet.getLoadedToolNames())).toEqual(new Set(['gamma', 'alpha', 'delta']));
94+
95+
const alphaState = workingSet.listLoadedTools().find((tool) => tool.name === 'alpha');
96+
expect(typeof alphaState?.lastAccessedAt).toBe('number');
97+
expect((alphaState?.lastAccessedAt ?? 0) > 0).toBe(true);
98+
99+
nowSpy.mockRestore();
86100
});
87101

88102
it('keeps always-loaded tools visible and protected from eviction or full unload', () => {
@@ -181,9 +195,13 @@ describe('SessionToolWorkingSet', () => {
181195
workingSet.loadTool('alpha');
182196
workingSet.loadTool('beta');
183197

184-
workingSet.reconfigure({ maxLoadedTools: 32, maxHydratedSchemas: 16 });
198+
workingSet.reconfigure({ maxLoadedTools: 32, maxHydratedSchemas: 16, idleEvictionThresholdMs: 90_000 });
185199

186-
expect(workingSet.getLimits()).toEqual({ maxLoadedTools: 32, maxHydratedSchemas: 16 });
200+
expect(workingSet.getLimits()).toEqual({
201+
maxLoadedTools: 32,
202+
maxHydratedSchemas: 16,
203+
idleEvictionThresholdMs: 90_000,
204+
});
187205
// Previously loaded tools are still present.
188206
expect(workingSet.isLoaded('alpha')).toBe(true);
189207
expect(workingSet.isLoaded('beta')).toBe(true);
@@ -192,10 +210,11 @@ describe('SessionToolWorkingSet', () => {
192210
it('reconfigure clamps inputs to valid bounds', () => {
193211
const workingSet = new SessionToolWorkingSet();
194212

195-
workingSet.reconfigure({ maxLoadedTools: 200, maxHydratedSchemas: 0 });
213+
workingSet.reconfigure({ maxLoadedTools: 200, maxHydratedSchemas: 0, idleEvictionThresholdMs: 1 });
196214

197215
const limits = workingSet.getLimits();
198216
expect(limits.maxLoadedTools).toBe(64); // clamped to max
199217
expect(limits.maxHydratedSchemas).toBe(2); // clamped to min
218+
expect(limits.idleEvictionThresholdMs).toBe(10_000); // clamped to min
200219
});
201220
});

0 commit comments

Comments
 (0)