@engineeros/connector 0.6.0 → 0.6.2

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.
@@ -19,6 +19,11 @@ import {
19
19
  stopProcess,
20
20
  workspaceSnapshot,
21
21
  } from "../src/runner.mjs";
22
+ import {
23
+ describeWebSocketError,
24
+ startConnectionWatchdog,
25
+ } from "../src/connection.mjs";
26
+ import { advertisedCapabilities } from "../src/capabilities.mjs";
22
27
 
23
28
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
24
29
 
@@ -59,13 +64,7 @@ if (command === "pair") {
59
64
  type: "pair",
60
65
  pairing_code: pairingCode,
61
66
  name: config.name,
62
- capabilities: {
63
- agent_protocols: flags["agent-command"] ? ["acp"] : ["codex"],
64
- coding_agent: true,
65
- codex_cli: !flags["agent-command"],
66
- platform: process.platform,
67
- workspace_name: path.basename(config.workspace),
68
- },
67
+ capabilities: {},
69
68
  };
70
69
  } else if (command === "start") {
71
70
  config = await loadConfig(flags.workspace || process.cwd());
@@ -89,10 +88,8 @@ try {
89
88
  fail(error instanceof Error ? error.message : String(error));
90
89
  }
91
90
  console.log(`Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`);
92
- if (firstMessage.type === "pair") {
93
- firstMessage.capabilities.agent_name = codingAgent.name;
94
- firstMessage.capabilities.agent_version = codingAgent.version;
95
- }
91
+ const capabilities = advertisedCapabilities(config, codingAgent);
92
+ firstMessage.capabilities = capabilities;
96
93
 
97
94
  let stopped = false;
98
95
  let active = null;
@@ -101,12 +98,18 @@ const assessments = [];
101
98
  const prompts = [];
102
99
  let socket;
103
100
  let pingTimer;
101
+ let reconnectTimer;
102
+ let clearConnectionWatchdog;
104
103
  let reconnectDelay = 1_000;
105
104
  let connectionRejected = false;
105
+ let lastConnectionError;
106
106
  let snapshotInFlight = false;
107
107
 
108
108
  process.on("SIGINT", async () => {
109
109
  stopped = true;
110
+ clearConnectionWatchdog?.();
111
+ clearTimeout(reconnectTimer);
112
+ clearInterval(pingTimer);
110
113
  await stopProcess(active?.child);
111
114
  socket?.close();
112
115
  process.exit(0);
@@ -117,13 +120,22 @@ await connect();
117
120
  async function connect() {
118
121
  console.log(`Connecting ${config.name} to ${config.server_url}`);
119
122
  connectionRejected = false;
123
+ lastConnectionError = undefined;
120
124
  socket = new WebSocket(config.server_url);
125
+ clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
126
+ onTimeout: (message) => {
127
+ lastConnectionError = message;
128
+ console.error(message);
129
+ scheduleReconnect();
130
+ },
131
+ });
121
132
  socket.addEventListener("open", () =>
122
133
  socket.send(JSON.stringify(firstMessage)),
123
134
  );
124
135
  socket.addEventListener("message", async (event) => {
125
136
  const message = JSON.parse(String(event.data));
126
137
  if (message.type === "paired") {
138
+ clearConnectionWatchdog?.();
127
139
  config = {
128
140
  ...config,
129
141
  connector_id: message.connector.id,
@@ -134,6 +146,7 @@ async function connect() {
134
146
  type: "authenticate",
135
147
  connector_id: config.connector_id,
136
148
  token: config.token,
149
+ capabilities,
137
150
  };
138
151
  console.log(`Paired. Connector ${config.connector_id} is online.`);
139
152
  reconnectDelay = 1_000;
@@ -142,6 +155,7 @@ async function connect() {
142
155
  return;
143
156
  }
144
157
  if (message.type === "authenticated") {
158
+ clearConnectionWatchdog?.();
145
159
  console.log("Connected and waiting for EngineerOS runs.");
146
160
  reconnectDelay = 1_000;
147
161
  startPings();
@@ -220,6 +234,7 @@ async function connect() {
220
234
  }
221
235
  });
222
236
  socket.addEventListener("close", () => {
237
+ clearConnectionWatchdog?.();
223
238
  clearInterval(pingTimer);
224
239
  if (active?.kind === "prompt") {
225
240
  active.cancelled = true;
@@ -233,13 +248,27 @@ async function connect() {
233
248
  return;
234
249
  }
235
250
  if (stopped) return;
251
+ scheduleReconnect();
252
+ });
253
+ socket.addEventListener("error", (event) => {
254
+ lastConnectionError = describeWebSocketError(event);
236
255
  console.error(
237
- `Connection lost. Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
256
+ `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
238
257
  );
239
- setTimeout(connect, reconnectDelay);
240
- reconnectDelay = Math.min(30_000, reconnectDelay * 2);
241
258
  });
242
- socket.addEventListener("error", () => {});
259
+ }
260
+
261
+ function scheduleReconnect() {
262
+ if (stopped || connectionRejected || reconnectTimer) return;
263
+ const detail = lastConnectionError ? ` Last error: ${lastConnectionError}` : "";
264
+ console.error(
265
+ `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
266
+ );
267
+ reconnectTimer = setTimeout(() => {
268
+ reconnectTimer = undefined;
269
+ void connect();
270
+ }, reconnectDelay);
271
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
243
272
  }
244
273
 
245
274
  async function submitWorkspaceSnapshot() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "start": "node ./bin/engineeros-connector.mjs",
18
18
  "test": "node --test --test-concurrency=1",
19
- "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/runner.mjs"
19
+ "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/capabilities.mjs && node --check ./src/connection.mjs && node --check ./src/runner.mjs"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=22"
@@ -0,0 +1,13 @@
1
+ import path from "node:path";
2
+
3
+ export function advertisedCapabilities(config, codingAgent) {
4
+ return {
5
+ agent_protocols: [codingAgent.protocol],
6
+ coding_agent: true,
7
+ codex_cli: codingAgent.protocol === "codex",
8
+ platform: process.platform,
9
+ workspace_name: path.basename(config.workspace),
10
+ agent_name: codingAgent.name,
11
+ agent_version: codingAgent.version,
12
+ };
13
+ }
@@ -0,0 +1,26 @@
1
+ export function describeWebSocketError(event) {
2
+ return (
3
+ event?.error?.message ||
4
+ event?.message ||
5
+ "The WebSocket connection failed without providing an error detail."
6
+ );
7
+ }
8
+
9
+ export function startConnectionWatchdog(
10
+ socket,
11
+ url,
12
+ { timeoutMs = 15_000, onTimeout = console.error } = {},
13
+ ) {
14
+ const timer = setTimeout(() => {
15
+ onTimeout(
16
+ `EngineerOS did not complete the WebSocket handshake at ${url} within ${Math.round(timeoutMs / 1_000)} seconds. Check that the backend is running and the URL is reachable from this machine.`,
17
+ );
18
+ try {
19
+ socket.close();
20
+ } catch {
21
+ // Reconnect scheduling is owned by the caller.
22
+ }
23
+ }, timeoutMs);
24
+
25
+ return () => clearTimeout(timer);
26
+ }