@nekzus/liop 1.2.0-alpha.9 → 1.2.0

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 (50) hide show
  1. package/README.md +12 -3
  2. package/dist/bin/agent.js +222 -51
  3. package/dist/bridge/index.js +7 -6
  4. package/dist/bridge/stream.js +11 -11
  5. package/dist/client/index.js +46 -35
  6. package/dist/crypto/logic-image-id.d.ts +3 -0
  7. package/dist/crypto/logic-image-id.js +27 -0
  8. package/dist/crypto/verifier.js +7 -19
  9. package/dist/economy/estimator.d.ts +53 -0
  10. package/dist/economy/estimator.js +69 -0
  11. package/dist/economy/index.d.ts +5 -0
  12. package/dist/economy/index.js +3 -0
  13. package/dist/economy/otel.d.ts +38 -0
  14. package/dist/economy/otel.js +100 -0
  15. package/dist/economy/telemetry.d.ts +77 -0
  16. package/dist/economy/telemetry.js +224 -0
  17. package/dist/errors.d.ts +14 -0
  18. package/dist/errors.js +19 -0
  19. package/dist/gateway/hybrid.d.ts +3 -1
  20. package/dist/gateway/hybrid.js +38 -13
  21. package/dist/gateway/router.d.ts +25 -9
  22. package/dist/gateway/router.js +484 -133
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +3 -0
  25. package/dist/mesh/node.d.ts +16 -0
  26. package/dist/mesh/node.js +394 -113
  27. package/dist/prompts/adapters.d.ts +16 -0
  28. package/dist/prompts/adapters.js +55 -0
  29. package/dist/rpc/proto.js +2 -1
  30. package/dist/rpc/server.d.ts +1 -1
  31. package/dist/rpc/server.js +4 -3
  32. package/dist/rpc/tls.js +3 -2
  33. package/dist/sandbox/wasi.d.ts +1 -1
  34. package/dist/sandbox/wasi.js +43 -3
  35. package/dist/security/guardian.js +3 -2
  36. package/dist/security/zk.d.ts +2 -3
  37. package/dist/security/zk.js +22 -9
  38. package/dist/server/index.d.ts +53 -4
  39. package/dist/server/index.js +362 -49
  40. package/dist/server/pii.d.ts +12 -0
  41. package/dist/server/pii.js +90 -0
  42. package/dist/types.d.ts +16 -0
  43. package/dist/utils/logger.d.ts +21 -0
  44. package/dist/utils/logger.js +70 -0
  45. package/dist/utils/mcpCompact.d.ts +11 -0
  46. package/dist/utils/mcpCompact.js +29 -0
  47. package/dist/workers/logic-execution.d.ts +1 -1
  48. package/dist/workers/logic-execution.js +38 -20
  49. package/dist/workers/zk-verifier.js +37 -33
  50. package/package.json +14 -2
@@ -1,10 +1,18 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import { LiopVerifier } from "../crypto/verifier.js";
3
+ import { TokenTelemetryEngine } from "../economy/telemetry.js";
3
4
  import { Kyber768Wrapper } from "../rpc/crypto/kyber.js";
4
5
  import { liopV1 } from "../rpc/proto.js";
5
6
  import { createChannelCredentials } from "../rpc/tls.js";
6
- /** Time-to-live for cached manifests (seconds) */
7
- const MANIFEST_CACHE_TTL_S = 30;
7
+ import { log } from "../utils/logger.js";
8
+ import { mcpCompactToolDescriptions, stripVerboseLiopToolDescription, } from "../utils/mcpCompact.js";
9
+ /**
10
+ * Time-to-live for cached manifests (seconds).
11
+ * Aligned with libp2p Kademlia DHT TABLE_REFRESH_INTERVAL (5 minutes).
12
+ * Provider records in the DHT are valid for 48 hours (PROVIDERS_VALIDITY),
13
+ * so 300s is a conservative, network-friendly value.
14
+ */
15
+ const MANIFEST_CACHE_TTL_S = 300;
8
16
  /** Maximum number of DHT query retries for manifest discovery */
9
17
  const MANIFEST_DISCOVERY_RETRIES = 5;
10
18
  /**
@@ -28,6 +36,11 @@ export class LiopMcpRouter {
28
36
  verifier = new LiopVerifier();
29
37
  /** Callback when new remote tools are discovered */
30
38
  onToolsChanged;
39
+ /** Circuit-breaker state for peers that repeatedly fail manifest queries. */
40
+ manifestFailureState = new Map();
41
+ static MANIFEST_FAILURE_BASE_COOLDOWN_MS = 15_000;
42
+ static MANIFEST_FAILURE_MAX_COOLDOWN_MS = 5 * 60_000;
43
+ static MANIFEST_SKIP_LOG_THROTTLE_MS = 30_000;
31
44
  constructor(liopServer, meshNode = null, defaultRpcPort = 50051) {
32
45
  this.liopServer = liopServer;
33
46
  this.meshNode = meshNode;
@@ -49,34 +62,56 @@ export class LiopMcpRouter {
49
62
  return {
50
63
  peerId: this.meshNode?.getPeerId() || "unknown",
51
64
  grpcPort: this.defaultRpcPort,
52
- tools: [
53
- {
54
- name: "LiopMeshStatus",
55
- description: "LiopMeshStatus: Returns the current dynamic diagnostic status of the Zero-Trust Neural Mesh.",
56
- inputSchema: { type: "object", properties: {} },
57
- },
58
- ...remoteTools,
59
- ],
65
+ tools: [...remoteTools],
60
66
  resources,
61
67
  serverInfo: this.liopServer.getServerInfo(),
62
68
  };
63
69
  });
64
70
  // Proactively announce manifest capability to the mesh
65
71
  this.meshNode.announceManifest().catch((err) => {
66
- console.error(`[LIOP-Router] Failed to announce manifest: ${err instanceof Error ? err.message : String(err)}`);
72
+ log.info(`[LIOP-Router] Failed to announce manifest: ${err instanceof Error ? err.message : String(err)}`);
67
73
  });
68
74
  }
69
75
  }
76
+ shouldSkipManifestQuery(peerId) {
77
+ const state = this.manifestFailureState.get(peerId);
78
+ if (!state)
79
+ return false;
80
+ const now = Date.now();
81
+ if (now >= state.cooldownUntil)
82
+ return false;
83
+ if (now - state.lastSkipLogAt >
84
+ LiopMcpRouter.MANIFEST_SKIP_LOG_THROTTLE_MS) {
85
+ log.info(`[LIOP-Router] Skipping manifest query for ${peerId} during cooldown (${Math.ceil((state.cooldownUntil - now) / 1000)}s remaining)`);
86
+ state.lastSkipLogAt = now;
87
+ }
88
+ return true;
89
+ }
90
+ recordManifestQuerySuccess(peerId) {
91
+ this.manifestFailureState.delete(peerId);
92
+ }
93
+ recordManifestQueryFailure(peerId) {
94
+ const now = Date.now();
95
+ const prev = this.manifestFailureState.get(peerId);
96
+ const failures = (prev?.failures || 0) + 1;
97
+ const backoff = Math.min(LiopMcpRouter.MANIFEST_FAILURE_BASE_COOLDOWN_MS *
98
+ 2 ** Math.max(0, failures - 1), LiopMcpRouter.MANIFEST_FAILURE_MAX_COOLDOWN_MS);
99
+ this.manifestFailureState.set(peerId, {
100
+ failures,
101
+ cooldownUntil: now + backoff,
102
+ lastSkipLogAt: 0,
103
+ });
104
+ }
70
105
  async dispatch(request) {
71
106
  const { method, params, id } = request;
72
- console.error(`[LIOP-Router] Processing: ${method}`);
107
+ log.info(`[LIOP-Router] Processing: ${method}`);
73
108
  switch (method) {
74
109
  case "initialize":
75
110
  return {
76
111
  jsonrpc: "2.0",
77
112
  id,
78
113
  result: {
79
- protocolVersion: "2025-03-26",
114
+ protocolVersion: "2025-11-25",
80
115
  capabilities: {
81
116
  tools: { listChanged: true },
82
117
  resources: { listChanged: true },
@@ -86,6 +121,10 @@ export class LiopMcpRouter {
86
121
  },
87
122
  };
88
123
  case "notifications/initialized":
124
+ // Cloud MCP clients often fire tools/list immediately; kick discovery early
125
+ // so manifests populate before (or right after) that call completes.
126
+ this.kickDiscoveryAfterInitialized().catch(() => { });
127
+ return null;
89
128
  case "notifications/cancelled":
90
129
  return null; // No-op for MCP spec compliance
91
130
  case "ping":
@@ -93,6 +132,13 @@ export class LiopMcpRouter {
93
132
  case "tools/list": {
94
133
  const localTools = this.liopServer.listTools();
95
134
  const remoteTools = await this.getRemoteTools();
135
+ const listedLocals = mcpCompactToolDescriptions()
136
+ ? localTools.map((t) => ({
137
+ ...t,
138
+ description: stripVerboseLiopToolDescription(t.description ?? ""),
139
+ }))
140
+ : localTools;
141
+ log.info(`[LIOP-Router] tools/list: ${localTools.length} local, ${remoteTools.length} remote tools found`);
96
142
  // Inject a mandatory static diagnostic tool.
97
143
  // This ensures that the {tools: []} list is never empty on startup.
98
144
  // Claude Desktop silently hides the connector if it receives an empty array initially,
@@ -100,12 +146,29 @@ export class LiopMcpRouter {
100
146
  const diagnosticTool = {
101
147
  name: "LiopMeshStatus",
102
148
  description: "LiopMeshStatus: Returns the current dynamic diagnostic status of the Zero-Trust Neural Mesh.",
103
- inputSchema: { type: "object", properties: {} },
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: {},
152
+ additionalProperties: false,
153
+ },
104
154
  };
155
+ const allTools = [diagnosticTool, ...listedLocals, ...remoteTools];
156
+ // [Token Economy] Record telemetry for the tools/list response
157
+ const telemetry = TokenTelemetryEngine.getInstance();
158
+ const toolsPayload = JSON.stringify(allTools);
159
+ const toolsResponsePayload = JSON.stringify({ tools: allTools });
160
+ telemetry.record({
161
+ type: "tools_list",
162
+ method: "tools/list",
163
+ estimatedInputTokens: telemetry.countTokens(toolsPayload),
164
+ estimatedOutputTokens: telemetry.countTokens(toolsResponsePayload),
165
+ });
105
166
  return {
106
167
  jsonrpc: "2.0",
107
168
  id,
108
- result: { tools: [diagnosticTool, ...localTools, ...remoteTools] },
169
+ result: {
170
+ tools: allTools,
171
+ },
109
172
  };
110
173
  }
111
174
  case "tools/call":
@@ -113,30 +176,53 @@ export class LiopMcpRouter {
113
176
  case "resources/list": {
114
177
  const localResources = this.liopServer.listResources();
115
178
  const remoteResources = await this.getRemoteResources();
179
+ const allResources = [...localResources, ...remoteResources];
180
+ // [Token Economy] Record resources/list telemetry
181
+ const rlTelemetry = TokenTelemetryEngine.getInstance();
182
+ const rlPayload = JSON.stringify(allResources);
183
+ rlTelemetry.record({
184
+ type: "resource_list",
185
+ method: "resources/list",
186
+ estimatedInputTokens: 0,
187
+ estimatedOutputTokens: rlTelemetry.countTokens(rlPayload),
188
+ });
116
189
  return {
117
190
  jsonrpc: "2.0",
118
191
  id,
119
- result: { resources: [...localResources, ...remoteResources] },
192
+ result: { resources: allResources },
120
193
  };
121
194
  }
122
195
  case "resources/read": {
123
- if (!params?.uri)
196
+ const typedParams = params;
197
+ if (!typedParams?.uri)
124
198
  return {
125
199
  jsonrpc: "2.0",
126
200
  id,
127
201
  error: { code: -32602, message: "Missing resource uri" },
128
202
  };
129
203
  try {
130
- const result = this.liopServer.readResource(params.uri);
204
+ const rrStartTime = Date.now();
205
+ const result = await this.liopServer.readResource(typedParams.uri);
206
+ // [Token Economy] Record resources/read telemetry
207
+ const rrTelemetry = TokenTelemetryEngine.getInstance();
208
+ const rrOutputPayload = JSON.stringify(result);
209
+ rrTelemetry.record({
210
+ type: "resource_read",
211
+ method: "resources/read",
212
+ toolName: typedParams.uri,
213
+ estimatedInputTokens: rrTelemetry.countTokens(typedParams.uri),
214
+ estimatedOutputTokens: rrTelemetry.countTokens(rrOutputPayload),
215
+ durationMs: Date.now() - rrStartTime,
216
+ });
131
217
  return { jsonrpc: "2.0", id, result };
132
218
  }
133
219
  catch (err) {
134
220
  // Fallback: Resolve remotely from manifest cache
135
- const targetUri = params.uri;
221
+ const targetUri = typedParams.uri;
136
222
  for (const { manifest } of this.manifestCache.values()) {
137
223
  const remoteResource = manifest.resources.find((r) => r.uri === targetUri);
138
224
  if (remoteResource) {
139
- console.error(`[LIOP-Router] Resolved resource ${targetUri} from cache (Peer: ${manifest.peerId})`);
225
+ log.info(`[LIOP-Router] Resolved resource ${targetUri} from cache (Peer: ${manifest.peerId})`);
140
226
  return {
141
227
  jsonrpc: "2.0",
142
228
  id,
@@ -164,12 +250,65 @@ export class LiopMcpRouter {
164
250
  };
165
251
  }
166
252
  }
167
- case "prompts/list":
253
+ case "prompts/list": {
254
+ const promptsList = this.liopServer.listPrompts();
255
+ // [Token Economy] Record prompts/list telemetry
256
+ const plTelemetry = TokenTelemetryEngine.getInstance();
257
+ const plPayload = JSON.stringify(promptsList);
258
+ plTelemetry.record({
259
+ type: "prompt_list",
260
+ method: "prompts/list",
261
+ estimatedInputTokens: 0,
262
+ estimatedOutputTokens: plTelemetry.countTokens(plPayload),
263
+ });
168
264
  return {
169
265
  jsonrpc: "2.0",
170
266
  id,
171
- result: { prompts: this.liopServer.listPrompts() },
267
+ result: { prompts: promptsList },
172
268
  };
269
+ }
270
+ case "prompts/get": {
271
+ const typedParams = params;
272
+ if (!typedParams?.name)
273
+ return {
274
+ jsonrpc: "2.0",
275
+ id,
276
+ error: { code: -32602, message: "Missing prompt name" },
277
+ };
278
+ try {
279
+ const pgStartTime = Date.now();
280
+ const result = await this.liopServer.getPrompt({
281
+ name: typedParams.name,
282
+ arguments: typedParams.arguments || {},
283
+ });
284
+ // [Token Economy] Record prompts/get telemetry
285
+ const pgTelemetry = TokenTelemetryEngine.getInstance();
286
+ const pgInputPayload = JSON.stringify({
287
+ name: typedParams.name,
288
+ arguments: typedParams.arguments,
289
+ });
290
+ const pgOutputPayload = JSON.stringify(result);
291
+ pgTelemetry.record({
292
+ type: "prompt_get",
293
+ method: "prompts/get",
294
+ toolName: typedParams.name,
295
+ estimatedInputTokens: pgTelemetry.countTokens(pgInputPayload),
296
+ estimatedOutputTokens: pgTelemetry.countTokens(pgOutputPayload),
297
+ durationMs: Date.now() - pgStartTime,
298
+ });
299
+ return { jsonrpc: "2.0", id, result };
300
+ }
301
+ catch (err) {
302
+ return {
303
+ jsonrpc: "2.0",
304
+ id,
305
+ error: {
306
+ code: -32000,
307
+ message: err instanceof Error ? err.message : String(err),
308
+ },
309
+ };
310
+ }
311
+ }
173
312
  default:
174
313
  return {
175
314
  jsonrpc: "2.0",
@@ -178,6 +317,19 @@ export class LiopMcpRouter {
178
317
  };
179
318
  }
180
319
  }
320
+ /**
321
+ * MCP clients often send notifications/initialized then immediately tools/list.
322
+ * Start manifest discovery without blocking the notification handler.
323
+ */
324
+ kickDiscoveryAfterInitialized() {
325
+ return (async () => {
326
+ await new Promise((r) => setTimeout(r, 250));
327
+ await Promise.race([
328
+ this.refreshManifestCache(true),
329
+ new Promise((r) => setTimeout(r, 15_000)),
330
+ ]).catch(() => { });
331
+ })();
332
+ }
181
333
  /**
182
334
  * Discovers and caches manifests from all remote LIOP providers in the mesh.
183
335
  * Uses Kademlia DHT to find "liop:manifest" providers, then opens
@@ -188,20 +340,29 @@ export class LiopMcpRouter {
188
340
  return;
189
341
  if (this.currentDiscovery)
190
342
  return this.currentDiscovery;
343
+ // Fast-path: Skip DHT query entirely when cache is fresh and populated.
344
+ // Only background polls (silent=true) should bypass this to detect new nodes.
345
+ // Foreground requests (tools/list, tools/call) can safely reuse valid cache.
346
+ if (!silent && this.manifestCache.size > 0) {
347
+ const now = Date.now();
348
+ const allFresh = Array.from(this.manifestCache.values()).every(({ cachedAt }) => now - cachedAt < MANIFEST_CACHE_TTL_S * 1000);
349
+ if (allFresh)
350
+ return;
351
+ }
191
352
  this.currentDiscovery = (async () => {
192
353
  try {
193
354
  const prevCount = Array.from(this.manifestCache.values()).reduce((acc, { manifest }) => acc + manifest.tools.length, 0);
194
355
  // Phase 0: Wait for at least one active connection if mesh is empty (Cold Start)
195
356
  if (this.manifestCache.size === 0) {
196
- for (let i = 0; i < 10; i++) {
357
+ for (let i = 0; i < 3; i++) {
197
358
  const connections =
198
359
  // biome-ignore lint/suspicious/noExplicitAny: access internal nodes for connection count
199
360
  this.meshNode.node?.getConnections().length || 0;
200
361
  if (connections > 0) {
201
- console.error(`[LIOP-Router] P2P Connection established. Starting discovery...`);
362
+ log.info(`[LIOP-Router] P2P Connection established. Starting discovery...`);
202
363
  break;
203
364
  }
204
- console.error(`[LIOP-Router] Waiting for P2P connections (attempt ${i + 1}/10)...`);
365
+ log.info(`[LIOP-Router] Waiting for P2P connections (attempt ${i + 1}/10)...`);
205
366
  await new Promise((r) => setTimeout(r, 1000));
206
367
  }
207
368
  }
@@ -213,77 +374,103 @@ export class LiopMcpRouter {
213
374
  for (let attempt = 0; attempt < MANIFEST_DISCOVERY_RETRIES; attempt++) {
214
375
  providerIds =
215
376
  (await this.meshNode?.discoverManifestProviders()) || [];
216
- if (providerIds.length > 0)
377
+ const selfId = this.meshNode?.getPeerId();
378
+ const remoteIds = providerIds.filter((id) => id !== selfId);
379
+ if (remoteIds.length > 0)
217
380
  break;
218
381
  if (attempt < MANIFEST_DISCOVERY_RETRIES - 1) {
219
- console.error(`[LIOP-Router] DHT discovery attempt ${attempt + 1}/${MANIFEST_DISCOVERY_RETRIES}...`);
382
+ log.info(`[LIOP-Router] DHT discovery attempt ${attempt + 1}/${MANIFEST_DISCOVERY_RETRIES}...`);
220
383
  await new Promise((r) => setTimeout(r, 1000));
221
384
  }
222
385
  }
223
- // 1.2 Fallback to all active connections
224
- if (providerIds.length === 0) {
225
- const activePeers =
226
- // biome-ignore lint/suspicious/noExplicitAny: access internal nodes
227
- this.meshNode.node
228
- ?.getConnections()
229
- .map((c) => c.remotePeer.toString()) || [];
230
- if (activePeers.length > 0) {
231
- console.error(`[LIOP-Router] DHT empty. Using ${activePeers.length} active connections as fallback.`);
232
- providerIds = activePeers;
233
- }
386
+ // 1.2 Aggressively merge all active connections to bypass DHT propagation delays
387
+ const activePeers =
388
+ // biome-ignore lint/suspicious/noExplicitAny: access internal nodes
389
+ this.meshNode.node
390
+ ?.getConnections()
391
+ .map((c) => c.remotePeer.toString()) || [];
392
+ if (activePeers.length > 0) {
393
+ providerIds = Array.from(new Set([...providerIds, ...activePeers]));
234
394
  }
235
- if (providerIds.length > 0)
395
+ const selfIdEnd = this.meshNode?.getPeerId();
396
+ const remoteIdsEnd = providerIds.filter((id) => id !== selfIdEnd);
397
+ if (remoteIdsEnd.length > 0)
236
398
  break;
237
399
  if (coldAttempt < MAX_COLD_ATTEMPTS - 1) {
238
- console.error(`[LIOP-Router] Initial discovery failed (0 providers). Retrying in 1s (${coldAttempt + 1}/${MAX_COLD_ATTEMPTS})...`);
400
+ log.info(`[LIOP-Router] Initial discovery failed (0 providers). Retrying in 1s (${coldAttempt + 1}/${MAX_COLD_ATTEMPTS})...`);
239
401
  await new Promise((r) => setTimeout(r, 1000));
240
402
  }
241
403
  }
242
404
  if (providerIds.length === 0) {
243
- console.error(`[LIOP-Router] No manifest providers found after all attempts.`);
405
+ log.info(`[LIOP-Router] No manifest providers found after all attempts.`);
244
406
  return;
245
407
  }
246
408
  if (!silent) {
247
- console.error(`[LIOP-Router] Discovered ${providerIds.length} candidate manifest providers`);
409
+ log.info(`[LIOP-Router] Discovered ${providerIds.length} candidate manifest providers`);
248
410
  }
411
+ // Prioritize already-connected peers to avoid blocking on stale providers.
412
+ // This improves first tools/list latency on Linux/Ubuntu while preserving
413
+ // full discovery for slower peers in subsequent refresh cycles.
414
+ const connectedPeers = new Set(
415
+ // biome-ignore lint/suspicious/noExplicitAny: internal node access for fast peer ordering
416
+ (this.meshNode.node?.getConnections?.() || []).map((c) => c.remotePeer.toString()));
417
+ providerIds = [...providerIds].sort((a, b) => {
418
+ const aConnected = connectedPeers.has(a) ? 1 : 0;
419
+ const bConnected = connectedPeers.has(b) ? 1 : 0;
420
+ return bConnected - aConnected;
421
+ });
249
422
  let successCount = 0;
250
423
  let errorCount = 0;
251
424
  let cacheUpdated = false;
252
- for (const peerId of providerIds) {
253
- // Avoid querying ourselves
254
- if (this.meshNode && peerId === this.meshNode.getPeerId())
255
- continue;
425
+ // Filter peers eligible for querying
426
+ const selfId = this.meshNode?.getPeerId();
427
+ const eligiblePeers = providerIds.filter((peerId) => {
256
428
  if (!this.meshNode)
257
- continue;
258
- // Skip if cached and not expired
429
+ return false;
430
+ if (peerId === selfId)
431
+ return false;
432
+ if (this.shouldSkipManifestQuery(peerId))
433
+ return false;
259
434
  const cached = this.manifestCache.get(peerId);
260
435
  if (cached &&
261
436
  Date.now() - cached.cachedAt < MANIFEST_CACHE_TTL_S * 1000) {
262
437
  successCount++;
263
- continue;
438
+ return false;
264
439
  }
265
- try {
266
- // Add a small delay between queries to avoid muxer saturation
267
- await new Promise((r) => setTimeout(r, 100));
268
- console.error(`[LIOP-Router] Querying manifest from: ${peerId}`);
269
- const manifest = await this.meshNode.queryManifest(peerId);
270
- if (manifest) {
271
- this.manifestCache.set(peerId, {
272
- manifest,
273
- cachedAt: Date.now(),
274
- });
275
- cacheUpdated = true;
276
- successCount++;
277
- console.error(`[LIOP-Router] Manifest received from ${peerId} (${manifest.tools.length} tools)`);
278
- }
279
- else {
280
- errorCount++;
281
- console.error(`[LIOP-Router] Manifest query returned NULL for ${peerId}`);
282
- }
440
+ return true;
441
+ });
442
+ // Parallel manifest queries eliminates sequential 100ms + retry delays
443
+ const queryResults = await Promise.allSettled(eligiblePeers.map(async (peerId) => {
444
+ if (!this.meshNode)
445
+ return null;
446
+ log.info(`[LIOP-Router] Querying manifest from: ${peerId}`);
447
+ return {
448
+ peerId,
449
+ manifest: await this.meshNode.queryManifest(peerId),
450
+ };
451
+ }));
452
+ for (const result of queryResults) {
453
+ if (result.status === "fulfilled" && result.value?.manifest) {
454
+ const { peerId, manifest } = result.value;
455
+ this.manifestCache.set(peerId, {
456
+ manifest,
457
+ cachedAt: Date.now(),
458
+ });
459
+ this.recordManifestQuerySuccess(peerId);
460
+ cacheUpdated = true;
461
+ successCount++;
462
+ log.info(`[LIOP-Router] Manifest received from ${peerId} (${manifest.tools.length} tools)`);
463
+ }
464
+ else if (result.status === "fulfilled" && result.value) {
465
+ this.recordManifestQueryFailure(result.value.peerId);
466
+ errorCount++;
467
+ log.info(`[LIOP-Router] Manifest query returned NULL for ${result.value.peerId}`);
283
468
  }
284
- catch (err) {
285
- console.error(`[LIOP-Router] Fatal error querying manifest from ${peerId}:`, err instanceof Error ? err.message : String(err));
469
+ else if (result.status === "rejected") {
286
470
  errorCount++;
471
+ log.info(`[LIOP-Router] Fatal error querying manifest:`, result.reason instanceof Error
472
+ ? result.reason.message
473
+ : String(result.reason));
287
474
  }
288
475
  }
289
476
  // Store discovery stats for LiopMeshStatus diagnostics
@@ -297,7 +484,7 @@ export class LiopMcpRouter {
297
484
  if (cacheUpdated) {
298
485
  const newCount = Array.from(this.manifestCache.values()).reduce((acc, { manifest }) => acc + manifest.tools.length, 0);
299
486
  if (newCount !== prevCount && this.onToolsChanged) {
300
- console.error("[LIOP-Router] Mesh topology updated! Emitting notifications/tools/list_changed.");
487
+ process.stderr.write("[LIOP-Router] Mesh topology updated! Emitting notifications/tools/list_changed.\n");
301
488
  this.onToolsChanged();
302
489
  }
303
490
  }
@@ -308,45 +495,123 @@ export class LiopMcpRouter {
308
495
  })();
309
496
  return this.currentDiscovery;
310
497
  }
498
+ /**
499
+ * Returns the current manifest cache size for external telemetry.
500
+ * Used by the adaptive polling system to detect topology stabilization.
501
+ */
502
+ getCacheSize() {
503
+ return this.manifestCache.size;
504
+ }
311
505
  /**
312
506
  * Returns all remote tools discovered via the manifest protocol.
313
507
  */
314
508
  async getRemoteTools() {
315
- // Wait for initial discovery if cache is empty
316
- if (this.manifestCache.size === 0 && this.meshNode) {
317
- await this.refreshManifestCache(true);
509
+ const EXPECTED_PROVIDERS = Number.parseInt(process.env.LIOP_EXPECTED_PROVIDERS ?? "4", 10);
510
+ // [Phase 106] Smart Warm-up with Stabilization Detection
511
+ // Loops until EXPECTED_PROVIDERS are found, the deadline expires, or
512
+ // the provider count stabilizes (same count for 3 consecutive checks).
513
+ // This prevents a ~20s block when a node (e.g. Bank) is absent.
514
+ if (this.manifestCache.size < EXPECTED_PROVIDERS && this.meshNode) {
515
+ const initialTimeoutMs = Number.parseInt(process.env.LIOP_INITIAL_DISCOVERY_TIMEOUT_MS ?? "12000", 10);
516
+ const boundedTimeoutMs = Number.isFinite(initialTimeoutMs) && initialTimeoutMs > 0
517
+ ? initialTimeoutMs
518
+ : 12000;
519
+ const deadline = Date.now() + boundedTimeoutMs;
520
+ let stableCount = 0;
521
+ let lastCacheSize = -1;
522
+ while (Date.now() < deadline) {
523
+ if (this.manifestCache.size >= EXPECTED_PROVIDERS)
524
+ break;
525
+ await Promise.race([
526
+ this.refreshManifestCache(true),
527
+ new Promise((resolve) => setTimeout(resolve, 3000)),
528
+ ]).catch(() => { });
529
+ if (this.manifestCache.size >= EXPECTED_PROVIDERS)
530
+ break;
531
+ // Stabilization detection: exit early when provider count plateaus
532
+ if (this.manifestCache.size === lastCacheSize) {
533
+ stableCount++;
534
+ if (stableCount >= 3 && this.manifestCache.size > 0) {
535
+ log.info(`[LIOP-Router] Provider count stabilized at ${this.manifestCache.size}/${EXPECTED_PROVIDERS}. Proceeding with available mesh.`);
536
+ break;
537
+ }
538
+ }
539
+ else {
540
+ stableCount = 0;
541
+ lastCacheSize = this.manifestCache.size;
542
+ }
543
+ // Wait before the next iteration to avoid CPU spin
544
+ await new Promise((r) => setTimeout(r, 1000));
545
+ }
546
+ // Diagnostic warning for partial mesh availability
547
+ if (this.manifestCache.size < EXPECTED_PROVIDERS) {
548
+ log.info(`[LIOP-Router] ⚠️ Mesh partially available: ${this.manifestCache.size}/${EXPECTED_PROVIDERS} providers. Some tools may be unavailable. Check Docker containers.`);
549
+ // Trigger one more background refresh to catch late joiners
550
+ this.refreshManifestCache(true).catch(() => { });
551
+ }
318
552
  }
319
553
  // biome-ignore lint/suspicious/noExplicitAny: Tool schema is polymorphic
320
554
  const tools = [];
321
- const seenNames = new Set(this.liopServer.listTools().map((t) => t.name));
555
+ const seenNames = new Set();
556
+ const localToolNames = new Set(this.liopServer.listTools().map((t) => t.name));
322
557
  for (const [peerId, { manifest }] of this.manifestCache.entries()) {
323
558
  for (const tool of manifest.tools) {
324
- if (!seenNames.has(tool.name)) {
325
- const augmentedTool = { ...tool };
326
- const providerName = manifest.serverInfo?.name || "Unknown Provider";
327
- let blueprint = "";
328
- if (manifest.taxonomy) {
329
- blueprint = `\n\n[LIOP-PROTO: TAXONOMY]\nDomain: ${manifest.taxonomy.domain}\nClearance Tier: ${manifest.taxonomy.clearanceTier}`;
330
- if (manifest.taxonomy.executionTypes &&
331
- manifest.taxonomy.executionTypes.length > 0) {
332
- blueprint += `\nExecution Types: ${manifest.taxonomy.executionTypes.join(", ")}`;
333
- }
334
- }
335
- // LIOP Logic-on-Origin Detection:
336
- // If the tool has a 'payload' property, it requires the Full LIOP Envelope.
337
- let envelopeDoc = "";
338
- // biome-ignore lint/suspicious/noExplicitAny: internal schema extraction
339
- const properties = tool.inputSchema?.properties || {};
340
- if (properties.payload) {
341
- envelopeDoc = `\n\n[LIOP-PROTO-V1: LOGIC-ON-ORIGIN SPECIFICATION]\nCRITICAL: This tool requires a strictly formatted Logic-on-Origin payload. Failure to wrap JavaScript code within the LIOP envelope will result in a MalformedPayloadError.\n\nREQUIRED FORMAT:\nLIOP_MAGIC:0x00FF\nMANIFEST:{"target":"wasi_v1","name":"[ModuleName]","integrity_checks":true}\n---BEGIN_LOGIC---\n// Pure JavaScript logic. Access data via 'env.records'.\n// You MUST use 'return' to output results.\n---END_LOGIC---\n\nExecution Environment: Zero-Trust WASI Sandbox (Node.js Worker Pool).`;
342
- }
343
- const originStamp = `\n\n[LIOP-REMOTE-ORIGIN-METADATA]\nProvider: ${providerName}\nNetwork ID: ${peerId}${blueprint}${envelopeDoc}`;
344
- augmentedTool.description = augmentedTool.description
345
- ? `${augmentedTool.description}${originStamp}`
346
- : originStamp.trim();
347
- tools.push(augmentedTool);
348
- seenNames.add(tool.name);
559
+ // LiopMeshStatus is a local-only diagnostic — skip remote copies
560
+ if (tool.name === "LiopMeshStatus")
561
+ continue;
562
+ // [LIOP-STABILITY] Allow discovery of ALL remote tools.
563
+ // MCP Requires unique names per server session.
564
+ // In a P2P mesh, multiple nodes might expose the same tool (e.g. LiopMeshStatus).
565
+ // We suffix duplicate names with a short peer hash to ensure
566
+ // ALL tools from ALL providers are correctly registered and visible.
567
+ let finalName = tool.name;
568
+ if (seenNames.has(tool.name) || localToolNames.has(tool.name)) {
569
+ finalName = `${tool.name}_${peerId.slice(-4)}`;
570
+ }
571
+ seenNames.add(finalName);
572
+ const providerName = manifest.serverInfo?.name || "Unknown Provider";
573
+ // [SANITIZATION] Create a clean MCP-compliant tool object
574
+ const baseDesc = tool.description || `Remote tool from ${providerName}`;
575
+ const cleanTool = {
576
+ name: finalName,
577
+ description: mcpCompactToolDescriptions()
578
+ ? stripVerboseLiopToolDescription(baseDesc)
579
+ : baseDesc,
580
+ inputSchema: (tool.inputSchema || {
581
+ type: "object",
582
+ properties: {},
583
+ }),
584
+ };
585
+ // Ensure inputSchema has the mandatory 'type: object' for MCP compliance
586
+ if (typeof cleanTool.inputSchema === "object" &&
587
+ !cleanTool.inputSchema.type) {
588
+ cleanTool.inputSchema.type = "object";
589
+ }
590
+ if (typeof cleanTool.inputSchema === "object" &&
591
+ !cleanTool.inputSchema.properties) {
592
+ cleanTool.inputSchema.properties = {};
593
+ }
594
+ let blueprint = "";
595
+ if (manifest.taxonomy) {
596
+ blueprint = `\n[LIOP-DOMAIN: ${manifest.taxonomy.domain}]`;
597
+ }
598
+ // LIOP Logic-on-Origin Detection:
599
+ // biome-ignore lint/suspicious/noExplicitAny: polymorphic input schema
600
+ const properties = (cleanTool.inputSchema.properties || {});
601
+ let envelopeDoc = "";
602
+ if (!mcpCompactToolDescriptions() && properties.payload) {
603
+ envelopeDoc = `\n[REQUIRES: LIOP-PROTO-V1 ENVELOPE]`;
349
604
  }
605
+ // INDUSTRIAL REPLICATION: Highlight schema adherence blocks
606
+ if (!mcpCompactToolDescriptions() &&
607
+ cleanTool.description.includes("STRICT SCHEMA ADHERENCE")) {
608
+ cleanTool.description = cleanTool.description.replace("STRICT SCHEMA ADHERENCE:", "[INDUSTRIAL-REQUISITE] STRICT SCHEMA ADHERENCE (MANDATORY):");
609
+ }
610
+ const originStamp = mcpCompactToolDescriptions()
611
+ ? `\n(Peer: ${peerId.slice(-8)})${blueprint}`
612
+ : `\n(Origin: ${peerId.slice(-8)})${blueprint}${envelopeDoc}`;
613
+ cleanTool.description = `${cleanTool.description}${originStamp}`;
614
+ tools.push(cleanTool);
350
615
  }
351
616
  }
352
617
  return tools;
@@ -375,9 +640,16 @@ export class LiopMcpRouter {
375
640
  }
376
641
  }
377
642
  const originStamp = `\n\n[LIOP Zero-Trust Origin]\nProvider: ${providerName}\nNetwork ID: ${peerId}${blueprint}`;
378
- augmentedResource.description = augmentedResource.description
379
- ? `${augmentedResource.description}${originStamp}`
380
- : originStamp.trim();
643
+ // INDUSTRIAL REPLICATION: Mark schema resources clearly
644
+ if (augmentedResource.uri.startsWith("liop://schema/")) {
645
+ augmentedResource.name = `[SCHEMA] ${augmentedResource.name}`;
646
+ augmentedResource.description = `[CRITICAL SCHEMA] ${augmentedResource.description || "Data Dictionary for Zero-Shot Autonomy"}${originStamp}`;
647
+ }
648
+ else {
649
+ augmentedResource.description = augmentedResource.description
650
+ ? `${augmentedResource.description}${originStamp}`
651
+ : originStamp.trim();
652
+ }
381
653
  resources.push(augmentedResource);
382
654
  seenUris.add(resource.uri);
383
655
  }
@@ -386,15 +658,35 @@ export class LiopMcpRouter {
386
658
  return resources;
387
659
  }
388
660
  /**
389
- * Resolves the gRPC target (host:port) for a given tool name
390
- * by searching the manifest cache. Returns null if not found.
661
+ * Resolves the gRPC target (host:port) AND the peerId for a given tool name
662
+ * by searching the manifest cache. Supports exact names and suffixed names.
391
663
  */
392
- resolveGrpcTarget(toolName) {
393
- for (const { manifest } of this.manifestCache.values()) {
394
- const found = manifest.tools.some((t) => t.name === toolName);
395
- if (found) {
396
- // Resolve IP from the peer's active connection or use localhost
397
- return `127.0.0.1:${manifest.grpcPort}`;
664
+ resolveManifestTarget(toolName) {
665
+ // 1. Try exact match
666
+ for (const [peerId, { manifest }] of this.manifestCache.entries()) {
667
+ const tool = manifest.tools.find((t) => t.name === toolName);
668
+ if (tool) {
669
+ return {
670
+ peerId,
671
+ originalToolName: toolName,
672
+ };
673
+ }
674
+ }
675
+ // 2. Try suffixed match (tool_xxxx)
676
+ const parts = toolName.split("_");
677
+ if (parts.length > 1) {
678
+ const suffix = parts.pop();
679
+ const baseName = parts.join("_");
680
+ for (const [peerId, { manifest }] of this.manifestCache.entries()) {
681
+ if (peerId.endsWith(suffix || "")) {
682
+ const tool = manifest.tools.find((t) => t.name === baseName);
683
+ if (tool) {
684
+ return {
685
+ peerId,
686
+ originalToolName: baseName,
687
+ };
688
+ }
689
+ }
398
690
  }
399
691
  }
400
692
  return null;
@@ -404,7 +696,9 @@ export class LiopMcpRouter {
404
696
  const toolName = params.name;
405
697
  // Intercept the static diagnostic tool
406
698
  if (toolName === "LiopMeshStatus") {
407
- // Trigger a proactive refresh when status is requested to force discovery
699
+ // [INDUSTRIAL-FIX] Proactive warm-up: request a refresh when status is called.
700
+ // This ensures that even if the DHT was cold, the next status call (or tools/list)
701
+ // will have data.
408
702
  this.refreshManifestCache(true).catch(() => { });
409
703
  // biome-ignore lint/suspicious/noExplicitAny: private stats for telemetry
410
704
  const stats = this._discoveryStats || {
@@ -451,9 +745,20 @@ export class LiopMcpRouter {
451
745
  cachedTools > 0
452
746
  ? `\nDiscovered Remote Tools (Zero-Trust Origins):\n${cachedToolList}`
453
747
  : "\nNo remote tools discovered yet.",
748
+ // [Token Economy] Telemetry block (only appears when operations exist)
749
+ TokenTelemetryEngine.getInstance().formatStatusBlock(),
454
750
  ]
455
751
  .filter((line) => line !== "")
456
752
  .join("\n");
753
+ // [Token Economy] Record diagnostic output telemetry
754
+ const diagTelemetry = TokenTelemetryEngine.getInstance();
755
+ diagTelemetry.record({
756
+ type: "diagnostic",
757
+ method: "tools/call",
758
+ toolName: "LiopMeshStatus",
759
+ estimatedInputTokens: 0,
760
+ estimatedOutputTokens: diagTelemetry.countTokens(statusText),
761
+ });
457
762
  return {
458
763
  jsonrpc: "2.0",
459
764
  id,
@@ -471,7 +776,19 @@ export class LiopMcpRouter {
471
776
  .listTools()
472
777
  .some((t) => t.name === toolName);
473
778
  if (!isLocal && this.meshNode) {
474
- // Phase 1: Try DHT-based dynamic provider discovery
779
+ // Phase 1: Cache-first resolve directly from cached manifests (zero-latency)
780
+ // Per MCP spec, tools don't change between notifications/tools/list_changed.
781
+ let target = this.resolveManifestTarget(toolName);
782
+ // Phase 2: If not cached, trigger DHT refresh and retry
783
+ if (!target) {
784
+ await this.refreshManifestCache();
785
+ target = this.resolveManifestTarget(toolName);
786
+ }
787
+ if (target) {
788
+ log.info(`[LIOP-Router] Resolved ${toolName} via manifest cache (Peer: ${target.peerId}, Original: ${target.originalToolName})`);
789
+ return this.routeToRemoteProvider(id, target.originalToolName, target.peerId, params);
790
+ }
791
+ // Phase 2: Try DHT-based dynamic provider discovery (fallback for unsuffixed names)
475
792
  let providers = [];
476
793
  for (let i = 0; i < 3; i++) {
477
794
  providers = await this.meshNode.findProviders(toolName);
@@ -483,22 +800,27 @@ export class LiopMcpRouter {
483
800
  if (providers.length > 0) {
484
801
  return this.routeToRemoteProvider(id, toolName, providers[0], params);
485
802
  }
486
- // Phase 2: Resolve from cached manifests (no DHT needed)
487
- await this.refreshManifestCache();
488
- const grpcTarget = this.resolveGrpcTarget(toolName);
489
- if (grpcTarget) {
490
- console.error(`[LIOP-Router] Resolved ${toolName} via manifest cache -> ${grpcTarget}`);
491
- const manifestClient = new liopV1.LogicMesh(grpcTarget, createChannelCredentials());
492
- return this.performTranscoding(id, manifestClient, toolName, params);
493
- }
494
803
  }
495
804
  // If no remote provider found, try local execution
496
805
  if (isLocal) {
497
806
  try {
807
+ const localStartTime = Date.now();
498
808
  const result = await this.liopServer.callTool({
499
809
  name: toolName,
500
810
  arguments: params.arguments || {},
501
811
  });
812
+ // [Token Economy] Record local tool call telemetry
813
+ const localTelemetry = TokenTelemetryEngine.getInstance();
814
+ const localInputPayload = JSON.stringify(params.arguments || {});
815
+ const localOutputPayload = JSON.stringify(result);
816
+ localTelemetry.record({
817
+ type: "tool_call",
818
+ method: "tools/call",
819
+ toolName,
820
+ estimatedInputTokens: localTelemetry.countTokens(localInputPayload),
821
+ estimatedOutputTokens: localTelemetry.countTokens(localOutputPayload),
822
+ durationMs: Date.now() - localStartTime,
823
+ });
502
824
  return { jsonrpc: "2.0", id, result };
503
825
  }
504
826
  catch (err) {
@@ -533,10 +855,10 @@ export class LiopMcpRouter {
533
855
  error: { code: -32603, message: "Mesh Node inactive" },
534
856
  };
535
857
  // Dynamic gRPC port resolution from manifest cache
536
- const cached = this.manifestCache.get(peerId);
858
+ let manifestEntry = this.manifestCache.get(peerId);
537
859
  let grpcPort = this.defaultRpcPort;
538
- if (cached) {
539
- grpcPort = cached.manifest.grpcPort;
860
+ if (manifestEntry) {
861
+ grpcPort = manifestEntry.manifest.grpcPort;
540
862
  }
541
863
  else {
542
864
  // Try to query the manifest directly
@@ -547,8 +869,21 @@ export class LiopMcpRouter {
547
869
  manifest,
548
870
  cachedAt: Date.now(),
549
871
  });
872
+ manifestEntry = this.manifestCache.get(peerId);
550
873
  }
551
874
  }
875
+ // Host-mode convenience (opt-in):
876
+ // Some Docker Desktop setups publish gRPC ports on the host as 13011/13021/13031.
877
+ // Inside Docker networks we must keep the manifest-advertised container port.
878
+ if (manifestEntry && process.env.LIOP_USE_PUBLISHED_GRPC_PORTS === "1") {
879
+ const providerName = manifestEntry.manifest.serverInfo?.name?.toLowerCase() || "";
880
+ if (providerName.includes("vault"))
881
+ grpcPort = 13011;
882
+ else if (providerName.includes("bank"))
883
+ grpcPort = 13021;
884
+ else if (providerName.includes("oracle"))
885
+ grpcPort = 13031;
886
+ }
552
887
  // Resolve IP from active connections
553
888
  const addrs = await this.meshNode.resolvePeer(peerId);
554
889
  let targetAddr = null;
@@ -581,9 +916,9 @@ export class LiopMcpRouter {
581
916
  // Fallback to localhost with the dynamically resolved port
582
917
  targetAddr = `127.0.0.1:${grpcPort}`;
583
918
  }
584
- console.error(`[LIOP-Router] Dynamic route: ${toolName} -> ${targetAddr} (PeerID: ${peerId})`);
919
+ log.info(`[LIOP-Router] Dynamic route: ${toolName} -> ${targetAddr} (PeerID: ${peerId})`);
585
920
  const remoteClient = new liopV1.LogicMesh(targetAddr, createChannelCredentials());
586
- return this.performTranscoding(id, remoteClient, toolName, params);
921
+ return this.performTranscoding(id, remoteClient, toolName, params, peerId);
587
922
  }
588
923
  async performTranscoding(
589
924
  // biome-ignore lint/suspicious/noExplicitAny: MCP polymorphic
@@ -591,14 +926,17 @@ export class LiopMcpRouter {
591
926
  // biome-ignore lint/suspicious/noExplicitAny: gRPC client from dynamic proto-loader
592
927
  client, toolName,
593
928
  // biome-ignore lint/suspicious/noExplicitAny: MCP polymorphic
594
- params) {
929
+ params, peerId) {
930
+ const capabilityHash = toolName;
931
+ const proofOfIntent = this.meshNode
932
+ ? await this.meshNode.sign(Buffer.from(capabilityHash))
933
+ : Buffer.from([]);
934
+ const transcodingStartTime = Date.now();
595
935
  return new Promise((resolve) => {
596
- // Using direct tool name for hash parity in Alpha v1
597
- const capabilityHash = toolName;
598
936
  client.negotiateIntent({
599
- agent_did: "did:liop:identity:mcp:proxy",
937
+ agent_did: `did:liop:${this.meshNode?.getPeerId() || "mcp-proxy"}`,
600
938
  capability_hash: capabilityHash,
601
- proof_of_intent: Buffer.from([]),
939
+ proof_of_intent: proofOfIntent,
602
940
  }, async (err, response) => {
603
941
  if (err || !response.accepted) {
604
942
  return resolve({
@@ -616,14 +954,16 @@ export class LiopMcpRouter {
616
954
  });
617
955
  }
618
956
  const { ciphertext, sharedSecret } = await Kyber768Wrapper.encapsulateAsymmetric(response.kyber_public_key);
619
- const proxyLogic = `return { "__liop_proxy_tool": "${toolName}", "__liop_proxy_args": env.args };`;
957
+ // SECURITY: Avoid AES-GCM nonce reuse across multiple ciphertexts.
958
+ // We embed arguments directly into the proxy logic so we only encrypt ONE payload per session/nonce.
959
+ const embeddedArgsJson = JSON.stringify(params.arguments || {});
960
+ const proxyLogic = `return { "__liop_proxy_tool": "${toolName}", "__liop_proxy_args": ${embeddedArgsJson} };`;
620
961
  const nonce = crypto.randomBytes(12);
621
962
  const sealedLogic = this.encryptWithNonce(Buffer.from(proxyLogic), sharedSecret, nonce);
622
- const sealedArgs = this.encryptWithNonce(Buffer.from(JSON.stringify(params.arguments || {})), sharedSecret, nonce);
623
963
  const call = client.executeLogic({
624
964
  session_token: response.session_token,
625
965
  wasm_binary: new Uint8Array(sealedLogic),
626
- inputs: { args: new Uint8Array(sealedArgs) },
966
+ inputs: {},
627
967
  pqc_ciphertext: ciphertext,
628
968
  aes_nonce: nonce,
629
969
  });
@@ -654,6 +994,17 @@ export class LiopMcpRouter {
654
994
  }
655
995
  }
656
996
  const parsedResult = JSON.parse(resultBody);
997
+ // [Token Economy] Record remote tool call telemetry
998
+ const remoteTelemetry = TokenTelemetryEngine.getInstance();
999
+ remoteTelemetry.record({
1000
+ type: "tool_call",
1001
+ method: "tools/call",
1002
+ toolName,
1003
+ peerId,
1004
+ estimatedInputTokens: remoteTelemetry.countTokens(embeddedArgsJson),
1005
+ estimatedOutputTokens: remoteTelemetry.countTokens(resultBody),
1006
+ durationMs: Date.now() - transcodingStartTime,
1007
+ });
657
1008
  resolve({ jsonrpc: "2.0", id, result: parsedResult });
658
1009
  }
659
1010
  catch (_e) {