@corbat-tech/coco 2.27.4 → 2.28.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.
package/dist/index.d.ts CHANGED
@@ -963,6 +963,8 @@ interface ToolUseContent {
963
963
  id: string;
964
964
  name: string;
965
965
  input: Record<string, unknown>;
966
+ /** Gemini-specific: preserve function-call thought signature across tool turns */
967
+ geminiThoughtSignature?: string;
966
968
  }
967
969
  /**
968
970
  * Tool result content block
@@ -999,6 +1001,8 @@ interface ToolCall {
999
1001
  id: string;
1000
1002
  name: string;
1001
1003
  input: Record<string, unknown>;
1004
+ /** Gemini-specific: preserve function-call thought signature across tool turns */
1005
+ geminiThoughtSignature?: string;
1002
1006
  }
1003
1007
  /**
1004
1008
  * Chat options
package/dist/index.js CHANGED
@@ -8,11 +8,11 @@ import fs16__default, { access, readFile, readdir, writeFile, mkdir } from 'fs/p
8
8
  import { randomUUID, randomBytes, createHash } from 'crypto';
9
9
  import * as http from 'http';
10
10
  import { fileURLToPath, URL as URL$1 } from 'url';
11
+ import { exec, execFile, execSync, spawn } from 'child_process';
12
+ import { promisify } from 'util';
11
13
  import { z } from 'zod';
12
14
  import * as p4 from '@clack/prompts';
13
15
  import chalk5 from 'chalk';
14
- import { exec, execFile, execSync, spawn } from 'child_process';
15
- import { promisify } from 'util';
16
16
  import { homedir } from 'os';
17
17
  import JSON5 from 'json5';
18
18
  import { execa } from 'execa';
@@ -676,6 +676,15 @@ async function exchangeForCopilotToken(githubToken) {
676
676
  }
677
677
  return await response.json();
678
678
  }
679
+ async function getGitHubCliToken() {
680
+ try {
681
+ const { stdout } = await execFileAsync("gh", ["auth", "token"], { timeout: 5e3 });
682
+ const token = stdout.trim();
683
+ return token.length > 0 ? token : null;
684
+ } catch {
685
+ return null;
686
+ }
687
+ }
679
688
  function getCopilotBaseUrl(accountType) {
680
689
  if (accountType && accountType in COPILOT_BASE_URLS) {
681
690
  return COPILOT_BASE_URLS[accountType];
@@ -713,10 +722,11 @@ function isCopilotTokenExpired(creds) {
713
722
  }
714
723
  async function getValidCopilotToken() {
715
724
  const creds = await loadCopilotCredentials();
716
- if (!creds) return null;
717
- const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
718
- const githubToken = envToken || creds.githubToken;
719
- if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
725
+ const envToken = process.env["COPILOT_GITHUB_TOKEN"] || process.env["GH_TOKEN"] || process.env["GITHUB_TOKEN"];
726
+ const fallbackGhToken = await getGitHubCliToken();
727
+ const githubToken = envToken || creds?.githubToken || fallbackGhToken;
728
+ if (!githubToken) return null;
729
+ if (creds && !isCopilotTokenExpired(creds) && creds.copilotToken) {
720
730
  return {
721
731
  token: creds.copilotToken,
722
732
  baseUrl: getCopilotBaseUrl(creds.accountType),
@@ -726,11 +736,11 @@ async function getValidCopilotToken() {
726
736
  try {
727
737
  const copilotToken = await exchangeForCopilotToken(githubToken);
728
738
  const updatedCreds = {
729
- ...creds,
730
- githubToken: creds.githubToken,
739
+ ...creds ?? { githubToken },
740
+ githubToken: creds?.githubToken ?? githubToken,
731
741
  copilotToken: copilotToken.token,
732
742
  copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
733
- accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
743
+ accountType: copilotToken.annotations?.copilot_plan ?? creds?.accountType
734
744
  };
735
745
  await saveCopilotCredentials(updatedCreds);
736
746
  return {
@@ -746,7 +756,7 @@ async function getValidCopilotToken() {
746
756
  throw error;
747
757
  }
748
758
  }
749
- var COPILOT_TOKEN_URL, COPILOT_BASE_URLS, DEFAULT_COPILOT_BASE_URL, REFRESH_BUFFER_MS, CopilotAuthError, CopilotCredentialsSchema;
759
+ var COPILOT_TOKEN_URL, COPILOT_BASE_URLS, DEFAULT_COPILOT_BASE_URL, REFRESH_BUFFER_MS, execFileAsync, CopilotAuthError, CopilotCredentialsSchema;
750
760
  var init_copilot = __esm({
751
761
  "src/auth/copilot.ts"() {
752
762
  COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
@@ -757,6 +767,7 @@ var init_copilot = __esm({
757
767
  };
758
768
  DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
759
769
  REFRESH_BUFFER_MS = 6e4;
770
+ execFileAsync = promisify(execFile);
760
771
  CopilotAuthError = class extends Error {
761
772
  constructor(message, permanent) {
762
773
  super(message);
@@ -1482,7 +1493,7 @@ function getDefaultModel(provider) {
1482
1493
  case "anthropic":
1483
1494
  return process.env["ANTHROPIC_MODEL"] ?? "claude-opus-4-6";
1484
1495
  case "openai":
1485
- return process.env["OPENAI_MODEL"] ?? "gpt-5.4-codex";
1496
+ return process.env["OPENAI_MODEL"] ?? "gpt-5.3-codex";
1486
1497
  case "gemini":
1487
1498
  return process.env["GEMINI_MODEL"] ?? "gemini-3.1-pro-preview";
1488
1499
  case "vertex":
@@ -14107,7 +14118,7 @@ var ResponsesToolCallAssembler = class {
14107
14118
  };
14108
14119
 
14109
14120
  // src/providers/openai.ts
14110
- var DEFAULT_MODEL2 = "gpt-5.4-codex";
14121
+ var DEFAULT_MODEL2 = "gpt-5.3-codex";
14111
14122
  var CONTEXT_WINDOWS2 = {
14112
14123
  // OpenAI models
14113
14124
  "gpt-4o": 128e3,
@@ -15313,7 +15324,7 @@ function createKimiProvider(config) {
15313
15324
  init_errors();
15314
15325
  init_auth();
15315
15326
  var CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
15316
- var DEFAULT_MODEL3 = "gpt-5.4-codex";
15327
+ var DEFAULT_MODEL3 = "gpt-5.3-codex";
15317
15328
  var CONTEXT_WINDOWS3 = {
15318
15329
  "gpt-5.4-codex": 2e5,
15319
15330
  "gpt-5.3-codex": 2e5,
@@ -16089,6 +16100,7 @@ var CopilotProvider = class extends OpenAIProvider {
16089
16100
  // src/providers/gemini.ts
16090
16101
  init_errors();
16091
16102
  var DEFAULT_MODEL5 = "gemini-3.1-pro-preview";
16103
+ var SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
16092
16104
  var CONTEXT_WINDOWS5 = {
16093
16105
  "gemini-3.1-pro-preview": 1e6,
16094
16106
  "gemini-3.1-flash-lite-preview": 1e6,
@@ -16181,30 +16193,29 @@ var GeminiProvider = class {
16181
16193
  if (text) {
16182
16194
  yield { type: "text", text };
16183
16195
  }
16184
- const functionCalls = this.extractFunctionCalls(chunk);
16185
- for (const functionCall of functionCalls) {
16186
- const toolCallId = functionCall.id ?? `gemini_call_${++fallbackToolCounter}`;
16196
+ const toolCalls = this.extractToolCalls(chunk, { includeLegacyFunctionCalls: true });
16197
+ for (const toolCall of toolCalls) {
16198
+ const toolCallId = toolCall.id ?? `gemini_call_${++fallbackToolCounter}`;
16187
16199
  if (emittedToolIds.has(toolCallId)) continue;
16188
16200
  emittedToolIds.add(toolCallId);
16189
- const toolCall = {
16190
- id: toolCallId,
16191
- name: functionCall.name ?? "unknown_function",
16192
- input: functionCall.args ?? {}
16201
+ const normalizedToolCall = {
16202
+ ...toolCall,
16203
+ id: toolCallId
16193
16204
  };
16194
16205
  yield {
16195
16206
  type: "tool_use_start",
16196
16207
  toolCall: {
16197
- id: toolCall.id,
16198
- name: toolCall.name
16208
+ id: normalizedToolCall.id,
16209
+ name: normalizedToolCall.name
16199
16210
  }
16200
16211
  };
16201
16212
  yield {
16202
16213
  type: "tool_use_end",
16203
- toolCall
16214
+ toolCall: normalizedToolCall
16204
16215
  };
16205
16216
  }
16206
16217
  const finishReason = chunk.candidates?.[0]?.finishReason;
16207
- if (functionCalls.length > 0) {
16218
+ if (toolCalls.length > 0) {
16208
16219
  streamStopReason = "tool_use";
16209
16220
  } else if (finishReason) {
16210
16221
  streamStopReason = this.mapFinishReason(finishReason);
@@ -16328,13 +16339,18 @@ var GeminiProvider = class {
16328
16339
  });
16329
16340
  } else if (block.type === "tool_use") {
16330
16341
  const toolUse = block;
16331
- parts.push({
16332
- functionCall: {
16333
- id: toolUse.id,
16334
- name: toolUse.name,
16335
- args: toolUse.input
16336
- }
16337
- });
16342
+ const thoughtSignature = toolUse.geminiThoughtSignature ?? SKIP_THOUGHT_SIGNATURE_VALIDATOR;
16343
+ const functionCall = {
16344
+ id: toolUse.id,
16345
+ name: toolUse.name,
16346
+ args: toolUse.input
16347
+ };
16348
+ const part = {
16349
+ functionCall,
16350
+ thoughtSignature,
16351
+ thought_signature: thoughtSignature
16352
+ };
16353
+ parts.push(part);
16338
16354
  }
16339
16355
  }
16340
16356
  return parts.length > 0 ? parts : [{ text: "" }];
@@ -16358,13 +16374,31 @@ var GeminiProvider = class {
16358
16374
  allowedFunctionNames: [choice.name]
16359
16375
  };
16360
16376
  }
16361
- extractFunctionCalls(response) {
16362
- if (response.functionCalls && response.functionCalls.length > 0) {
16363
- return response.functionCalls;
16377
+ extractThoughtSignatureFromPart(part) {
16378
+ const withSignature = part;
16379
+ return withSignature.thoughtSignature ?? withSignature.thought_signature ?? withSignature.functionCall?.thoughtSignature ?? withSignature.functionCall?.thought_signature;
16380
+ }
16381
+ extractToolCalls(response, options) {
16382
+ const toolCallsFromParts = (response.candidates?.[0]?.content?.parts ?? []).filter((part) => !!part.functionCall).map((part, index) => ({
16383
+ id: part.functionCall.id ?? `gemini_call_${index + 1}`,
16384
+ name: part.functionCall.name ?? "unknown_function",
16385
+ input: part.functionCall.args ?? {},
16386
+ geminiThoughtSignature: this.extractThoughtSignatureFromPart(part)
16387
+ }));
16388
+ if (toolCallsFromParts.length > 0) {
16389
+ return toolCallsFromParts;
16364
16390
  }
16365
- const candidate = response.candidates?.[0];
16366
- const parts = candidate?.content?.parts ?? [];
16367
- return parts.filter((part) => !!part.functionCall).map((part) => part.functionCall).filter(Boolean);
16391
+ if (!options?.includeLegacyFunctionCalls || !response.functionCalls?.length) {
16392
+ return [];
16393
+ }
16394
+ return response.functionCalls.map((functionCall, index) => ({
16395
+ id: functionCall.id ?? `gemini_call_${index + 1}`,
16396
+ name: functionCall.name ?? "unknown_function",
16397
+ input: functionCall.args ?? {},
16398
+ geminiThoughtSignature: this.extractThoughtSignatureFromPart({
16399
+ functionCall
16400
+ })
16401
+ }));
16368
16402
  }
16369
16403
  parseResponse(response, model) {
16370
16404
  const usage = response.usageMetadata;
@@ -16381,11 +16415,7 @@ var GeminiProvider = class {
16381
16415
  }
16382
16416
  parseResponseWithTools(response, model) {
16383
16417
  const usage = response.usageMetadata;
16384
- const toolCalls = this.extractFunctionCalls(response).map((functionCall, index) => ({
16385
- id: functionCall.id ?? `gemini_call_${index + 1}`,
16386
- name: functionCall.name ?? "unknown_function",
16387
- input: functionCall.args ?? {}
16388
- }));
16418
+ const toolCalls = this.extractToolCalls(response, { includeLegacyFunctionCalls: true });
16389
16419
  return {
16390
16420
  id: `gemini-${Date.now()}`,
16391
16421
  content: response.text ?? "",
@@ -16437,12 +16467,33 @@ var DEFAULT_MODEL6 = "gemini-2.5-pro";
16437
16467
  var DEFAULT_BASE_URL = "https://aiplatform.googleapis.com/v1";
16438
16468
  var DEFAULT_LOCATION = "global";
16439
16469
  var CONTEXT_WINDOWS6 = {
16470
+ "gemini-3-pro-preview": 1048576,
16471
+ "gemini-3-flash-preview": 1048576,
16440
16472
  "gemini-2.5-pro": 1048576,
16441
16473
  "gemini-2.5-flash": 1048576,
16442
16474
  "gemini-2.5-flash-lite": 1048576,
16443
16475
  "gemini-2.0-flash-001": 1048576,
16444
16476
  "gemini-2.0-flash-lite-001": 1048576
16445
16477
  };
16478
+ var SKIP_THOUGHT_SIGNATURE_VALIDATOR2 = "skip_thought_signature_validator";
16479
+ function extractSseEventData(rawEvent) {
16480
+ const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter(Boolean);
16481
+ if (dataLines.length === 0) {
16482
+ return null;
16483
+ }
16484
+ return dataLines.join("\n");
16485
+ }
16486
+ function getToolCallFingerprint(part) {
16487
+ const name = part.functionCall?.name ?? "unknown_function";
16488
+ let argsSerialized = "{}";
16489
+ try {
16490
+ argsSerialized = JSON.stringify(part.functionCall?.args ?? {});
16491
+ } catch {
16492
+ argsSerialized = "{}";
16493
+ }
16494
+ const thoughtSignature = part.thoughtSignature ?? part.thought_signature ?? part.functionCall?.thoughtSignature ?? part.functionCall?.thought_signature ?? "";
16495
+ return `${name}:${argsSerialized}:${thoughtSignature}`;
16496
+ }
16446
16497
  var VertexProvider = class {
16447
16498
  id = "vertex";
16448
16499
  name = "Google Vertex AI Gemini";
@@ -16518,6 +16569,7 @@ var VertexProvider = class {
16518
16569
  );
16519
16570
  let stopReason = "end_turn";
16520
16571
  let streamToolCallCounter = 0;
16572
+ const emittedToolFingerprints = /* @__PURE__ */ new Set();
16521
16573
  for await (const chunk of stream) {
16522
16574
  const candidate = chunk.candidates?.[0];
16523
16575
  const parts = candidate?.content?.parts ?? [];
@@ -16526,13 +16578,20 @@ var VertexProvider = class {
16526
16578
  yield { type: "text", text: part.text };
16527
16579
  }
16528
16580
  if (part.functionCall) {
16581
+ const fingerprint = getToolCallFingerprint(part);
16582
+ if (emittedToolFingerprints.has(fingerprint)) {
16583
+ continue;
16584
+ }
16585
+ emittedToolFingerprints.add(fingerprint);
16529
16586
  streamToolCallCounter++;
16587
+ const geminiThoughtSignature = part.thoughtSignature ?? part.thought_signature ?? part.functionCall.thoughtSignature ?? part.functionCall.thought_signature;
16530
16588
  yield {
16531
16589
  type: "tool_use_start",
16532
16590
  toolCall: {
16533
16591
  id: `vertex_call_${streamToolCallCounter}`,
16534
16592
  name: part.functionCall.name,
16535
- input: part.functionCall.args ?? {}
16593
+ input: part.functionCall.args ?? {},
16594
+ geminiThoughtSignature
16536
16595
  }
16537
16596
  };
16538
16597
  yield {
@@ -16540,7 +16599,8 @@ var VertexProvider = class {
16540
16599
  toolCall: {
16541
16600
  id: `vertex_call_${streamToolCallCounter}`,
16542
16601
  name: part.functionCall.name,
16543
- input: part.functionCall.args ?? {}
16602
+ input: part.functionCall.args ?? {},
16603
+ geminiThoughtSignature
16544
16604
  }
16545
16605
  };
16546
16606
  }
@@ -16673,11 +16733,14 @@ var VertexProvider = class {
16673
16733
  });
16674
16734
  } else if (block.type === "tool_use") {
16675
16735
  const toolUse = block;
16736
+ const thoughtSignature = toolUse.geminiThoughtSignature ?? SKIP_THOUGHT_SIGNATURE_VALIDATOR2;
16676
16737
  parts.push({
16677
16738
  functionCall: {
16678
16739
  name: toolUse.name,
16679
16740
  args: toolUse.input
16680
- }
16741
+ },
16742
+ thoughtSignature,
16743
+ thought_signature: thoughtSignature
16681
16744
  });
16682
16745
  }
16683
16746
  }
@@ -16769,22 +16832,27 @@ var VertexProvider = class {
16769
16832
  if (done) break;
16770
16833
  buffer += decoder.decode(value, { stream: true });
16771
16834
  while (true) {
16772
- const eventBoundary = buffer.indexOf("\n\n");
16773
- if (eventBoundary === -1) break;
16774
- const rawEvent = buffer.slice(0, eventBoundary);
16775
- buffer = buffer.slice(eventBoundary + 2);
16776
- const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter(Boolean);
16777
- for (const line of dataLines) {
16778
- if (line === "[DONE]") return;
16779
- yield JSON.parse(line);
16835
+ const boundaryMatch = /\r?\n\r?\n/.exec(buffer);
16836
+ if (!boundaryMatch || boundaryMatch.index === void 0) break;
16837
+ const rawEvent = buffer.slice(0, boundaryMatch.index);
16838
+ buffer = buffer.slice(boundaryMatch.index + boundaryMatch[0].length);
16839
+ const data = extractSseEventData(rawEvent);
16840
+ if (!data || data === "[DONE]") {
16841
+ if (data === "[DONE]") return;
16842
+ continue;
16843
+ }
16844
+ try {
16845
+ yield JSON.parse(data);
16846
+ } catch {
16847
+ continue;
16780
16848
  }
16781
16849
  }
16782
16850
  }
16783
- const trailing = buffer.trim();
16784
- if (trailing.startsWith("data:")) {
16785
- const line = trailing.slice(5).trim();
16786
- if (line && line !== "[DONE]") {
16787
- yield JSON.parse(line);
16851
+ const trailingData = extractSseEventData(buffer.trim());
16852
+ if (trailingData && trailingData !== "[DONE]") {
16853
+ try {
16854
+ yield JSON.parse(trailingData);
16855
+ } catch {
16788
16856
  }
16789
16857
  }
16790
16858
  }
@@ -16817,7 +16885,8 @@ var VertexProvider = class {
16817
16885
  toolCalls.push({
16818
16886
  id: `vertex_call_${toolIndex}`,
16819
16887
  name: part.functionCall.name,
16820
- input: part.functionCall.args ?? {}
16888
+ input: part.functionCall.args ?? {},
16889
+ geminiThoughtSignature: part.thoughtSignature ?? part.thought_signature ?? part.functionCall.thoughtSignature ?? part.functionCall.thought_signature
16821
16890
  });
16822
16891
  }
16823
16892
  }
@@ -21448,9 +21517,6 @@ var buildTools = [
21448
21517
  runGradleTool
21449
21518
  ];
21450
21519
 
21451
- // src/cli/repl/recommended-permissions.ts
21452
- init_paths();
21453
-
21454
21520
  // src/cli/repl/session.ts
21455
21521
  init_env();
21456
21522
  init_paths();
@@ -21485,6 +21551,7 @@ z.object({
21485
21551
  // src/cli/repl/session.ts
21486
21552
  path17__default.dirname(CONFIG_PATHS.trustedTools);
21487
21553
  CONFIG_PATHS.trustedTools;
21554
+ path17__default.join(".coco", "trusted-tools.json");
21488
21555
 
21489
21556
  // src/cli/repl/recommended-permissions.ts
21490
21557
  var RECOMMENDED_GLOBAL = [
@@ -27503,7 +27570,7 @@ init_errors2();
27503
27570
  init_callback_server();
27504
27571
  init_paths();
27505
27572
  init_logger();
27506
- var execFileAsync2 = promisify(execFile);
27573
+ var execFileAsync3 = promisify(execFile);
27507
27574
  var TOKEN_STORE_PATH = path17__default.join(CONFIG_PATHS.tokens, "mcp-oauth.json");
27508
27575
  var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
27509
27576
  var logger = getLogger();
@@ -27600,7 +27667,7 @@ async function openBrowser(url) {
27600
27667
  }
27601
27668
  for (const { cmd, args } of commands) {
27602
27669
  try {
27603
- await execFileAsync2(cmd, args);
27670
+ await execFileAsync3(cmd, args);
27604
27671
  return true;
27605
27672
  } catch {
27606
27673
  continue;
@@ -28476,6 +28543,25 @@ var MCPServerManager = class _MCPServerManager {
28476
28543
  connections = /* @__PURE__ */ new Map();
28477
28544
  logger = getLogger();
28478
28545
  static STOP_TIMEOUT_MS = 5e3;
28546
+ /**
28547
+ * Run an async operation with a timeout, always clearing timer resources.
28548
+ */
28549
+ async runWithTimeout(operation, timeoutMs, timeoutMessage) {
28550
+ let timeoutId;
28551
+ const timeoutPromise = new Promise((_, reject) => {
28552
+ timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
28553
+ if (typeof timeoutId.unref === "function") {
28554
+ timeoutId.unref();
28555
+ }
28556
+ });
28557
+ try {
28558
+ return await Promise.race([operation, timeoutPromise]);
28559
+ } finally {
28560
+ if (timeoutId) {
28561
+ clearTimeout(timeoutId);
28562
+ }
28563
+ }
28564
+ }
28479
28565
  /**
28480
28566
  * Create transport for a server config
28481
28567
  */
@@ -28562,15 +28648,11 @@ var MCPServerManager = class _MCPServerManager {
28562
28648
  }
28563
28649
  this.logger.info(`Stopping MCP server: ${name}`);
28564
28650
  try {
28565
- await Promise.race([
28651
+ await this.runWithTimeout(
28566
28652
  connection.transport.disconnect(),
28567
- new Promise(
28568
- (_, reject) => setTimeout(
28569
- () => reject(new Error("MCP disconnect timeout")),
28570
- _MCPServerManager.STOP_TIMEOUT_MS
28571
- )
28572
- )
28573
- ]);
28653
+ _MCPServerManager.STOP_TIMEOUT_MS,
28654
+ "MCP disconnect timeout"
28655
+ );
28574
28656
  } catch (error) {
28575
28657
  this.logger.error(
28576
28658
  `Error disconnecting server '${name}': ${error instanceof Error ? error.message : String(error)}`
@@ -28607,12 +28689,11 @@ var MCPServerManager = class _MCPServerManager {
28607
28689
  }
28608
28690
  const startTime = performance.now();
28609
28691
  try {
28610
- const { tools } = await Promise.race([
28692
+ const { tools } = await this.runWithTimeout(
28611
28693
  connection.client.listTools(),
28612
- new Promise(
28613
- (_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
28614
- )
28615
- ]);
28694
+ 5e3,
28695
+ "Health check timeout"
28696
+ );
28616
28697
  const latencyMs = performance.now() - startTime;
28617
28698
  connection.healthy = true;
28618
28699
  connection.toolCount = tools.length;