data fixes, partial custom indicator support

This commit is contained in:
2026-04-08 21:28:31 -04:00
parent b701554996
commit a70dcd954f
81 changed files with 5438 additions and 1852 deletions

View File

@@ -10,6 +10,7 @@ import type { SymbolIndexService } from '../services/symbol-index-service.js';
import type { ContainerManager } from '../k8s/container-manager.js';
import {
WorkspaceManager,
ContainerSync,
DEFAULT_STORES,
type ChannelAdapter,
type ChannelCapabilities,
@@ -120,15 +121,6 @@ export class WebSocketHandler {
sendStatus(socket, 'initializing', 'Starting your workspace...');
// Create workspace manager for this session
const workspace = new WorkspaceManager({
userId: authContext.userId,
sessionId: authContext.sessionId,
stores: DEFAULT_STORES,
// containerSync will be added when MCP client is implemented
logger,
});
// Create WebSocket channel adapter
const wsAdapter: ChannelAdapter = {
sendSnapshot: (msg: SnapshotMessage) => {
@@ -174,31 +166,47 @@ export class WebSocketHandler {
}),
};
// Declare harness outside try block so it's available in catch
// Declare harness and workspace outside try block so they're available in catch
let harness: AgentHarness | undefined;
let workspace: WorkspaceManager | undefined;
try {
// Initialize workspace first
await workspace.initialize();
workspace.setAdapter(wsAdapter);
this.workspaces.set(authContext.sessionId, workspace);
// Create agent harness via factory (storage deps injected by factory)
// Create and connect harness first so MCP client is available for ContainerSync
harness = this.config.createHarness({
userId: authContext.userId,
sessionId: authContext.sessionId,
license: authContext.license,
mcpServerUrl: authContext.mcpServerUrl,
logger,
workspaceManager: workspace,
channelAdapter: wsAdapter,
channelType: authContext.channelType,
channelUserId: authContext.channelUserId,
});
await harness.initialize();
// Wire ContainerSync now that MCP client is connected, then initialize workspace
const containerSync = new ContainerSync(harness.getMcpClient(), logger);
workspace = new WorkspaceManager({
userId: authContext.userId,
sessionId: authContext.sessionId,
stores: DEFAULT_STORES,
containerSync,
logger,
});
await workspace.initialize();
workspace.setAdapter(wsAdapter);
harness.setWorkspaceManager(workspace);
this.workspaces.set(authContext.sessionId, workspace);
this.harnesses.set(authContext.sessionId, harness);
// Push all store snapshots to the client now, before 'connected'.
// Empty seqs force full snapshots for every store, so the browser's
// message queue has the current workspace state (including persistent
// stores loaded from the container) before TradingView initializes.
await workspace.handleHello({});
// Register session for event system
// Container endpoint is derived from the MCP server URL (same container, different port)
const containerEventEndpoint = this.getContainerEventEndpoint(authContext.mcpServerUrl);
@@ -287,15 +295,18 @@ export class WebSocketHandler {
} else if (payload.type === 'hello') {
// Workspace sync: hello message
logger.debug({ seqs: payload.seqs }, 'Handling workspace hello');
await workspace.handleHello(payload.seqs || {});
await workspace!.handleHello(payload.seqs || {});
} else if (payload.type === 'patch') {
// Workspace sync: patch message
logger.debug({ store: payload.store, seq: payload.seq }, 'Handling workspace patch');
await workspace.handlePatch(payload.store, payload.seq, payload.patch || []);
await workspace!.handlePatch(payload.store, payload.seq, payload.patch || []);
} else if (payload.type === 'agent_stop') {
logger.info('Agent stop requested');
harness?.interrupt();
} else if (this.isDatafeedMessage(payload)) {
// Historical data request - send to OHLC service
logger.info({ type: payload.type }, 'Routing to datafeed handler');
await this.handleDatafeedMessage(socket, payload, logger);
await this.handleDatafeedMessage(socket, payload, logger, authContext);
} else {
logger.warn({ type: payload.type }, 'Unknown message type received');
}
@@ -322,7 +333,7 @@ export class WebSocketHandler {
}
// Cleanup workspace
await workspace.shutdown();
await workspace!.shutdown();
this.workspaces.delete(authContext.sessionId);
// Cleanup harness
@@ -346,8 +357,10 @@ export class WebSocketHandler {
} catch (error) {
logger.error({ error }, 'Failed to initialize session');
socket.close(1011, 'Internal server error');
await workspace.shutdown();
this.workspaces.delete(authContext.sessionId);
if (workspace) {
await workspace.shutdown();
this.workspaces.delete(authContext.sessionId);
}
if (harness) {
await harness.cleanup();
}
@@ -382,6 +395,7 @@ export class WebSocketHandler {
'get_bars',
'subscribe_bars',
'unsubscribe_bars',
'evaluate_indicator',
];
return datafeedTypes.includes(payload.type);
}
@@ -392,7 +406,8 @@ export class WebSocketHandler {
private async handleDatafeedMessage(
socket: WebSocket,
payload: any,
logger: any
logger: any,
authContext?: any
): Promise<void> {
logger.info({ type: payload.type, payload }, 'handleDatafeedMessage called');
const ohlcService = this.config.ohlcService;
@@ -526,6 +541,69 @@ export class WebSocketHandler {
);
break;
case 'evaluate_indicator': {
// Direct MCP call — bypasses the agent/LLM for performance
const harness = this.harnesses.get(authContext.sessionId);
if (!harness) {
socket.send(JSON.stringify({
type: 'evaluate_indicator_result',
request_id: requestId,
error: 'Session not initialized',
}));
break;
}
try {
const mcpResult = await harness.callMcpTool('evaluate_indicator', {
symbol: payload.symbol,
from_time: payload.from_time,
to_time: payload.to_time,
period_seconds: payload.period_seconds,
pandas_ta_name: payload.pandas_ta_name,
parameters: payload.parameters ?? {},
}) as any;
// MCP returns { content: [{type: 'text', text: '...json...'}] }
// When the tool raises an exception, the MCP framework sets isError: true
// and puts the raw exception text in content[0].text (not JSON-wrapped).
const rawText = mcpResult?.content?.[0]?.text ?? mcpResult?.[0]?.text;
if (mcpResult?.isError || rawText == null) {
const errMsg = rawText ?? 'evaluate_indicator returned no content';
logger.error({ pandas_ta_name: payload.pandas_ta_name, rawText }, 'evaluate_indicator sandbox error');
socket.send(JSON.stringify({
type: 'evaluate_indicator_result',
request_id: requestId,
error: errMsg,
}));
break;
}
let data: any;
try {
data = JSON.parse(rawText);
} catch {
// Sandbox returned non-JSON (e.g. bare exception text)
logger.error({ pandas_ta_name: payload.pandas_ta_name, rawText }, 'evaluate_indicator returned non-JSON');
socket.send(JSON.stringify({
type: 'evaluate_indicator_result',
request_id: requestId,
error: rawText,
}));
break;
}
socket.send(JSON.stringify({
type: 'evaluate_indicator_result',
request_id: requestId,
...data,
}));
} catch (err: any) {
logger.error({ err: err?.message, pandas_ta_name: payload.pandas_ta_name }, 'evaluate_indicator handler error');
socket.send(JSON.stringify({
type: 'evaluate_indicator_result',
request_id: requestId,
error: err?.message ?? String(err),
}));
}
break;
}
default:
logger.warn({ type: payload.type }, 'Unknown datafeed message type');
}