@engineeros/connector 0.6.0 → 0.6.1

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,10 @@ import {
19
19
  stopProcess,
20
20
  workspaceSnapshot,
21
21
  } from "../src/runner.mjs";
22
+ import {
23
+ describeWebSocketError,
24
+ startConnectionWatchdog,
25
+ } from "../src/connection.mjs";
22
26
 
23
27
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
24
28
 
@@ -101,12 +105,18 @@ const assessments = [];
101
105
  const prompts = [];
102
106
  let socket;
103
107
  let pingTimer;
108
+ let reconnectTimer;
109
+ let clearConnectionWatchdog;
104
110
  let reconnectDelay = 1_000;
105
111
  let connectionRejected = false;
112
+ let lastConnectionError;
106
113
  let snapshotInFlight = false;
107
114
 
108
115
  process.on("SIGINT", async () => {
109
116
  stopped = true;
117
+ clearConnectionWatchdog?.();
118
+ clearTimeout(reconnectTimer);
119
+ clearInterval(pingTimer);
110
120
  await stopProcess(active?.child);
111
121
  socket?.close();
112
122
  process.exit(0);
@@ -117,13 +127,22 @@ await connect();
117
127
  async function connect() {
118
128
  console.log(`Connecting ${config.name} to ${config.server_url}`);
119
129
  connectionRejected = false;
130
+ lastConnectionError = undefined;
120
131
  socket = new WebSocket(config.server_url);
132
+ clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
133
+ onTimeout: (message) => {
134
+ lastConnectionError = message;
135
+ console.error(message);
136
+ scheduleReconnect();
137
+ },
138
+ });
121
139
  socket.addEventListener("open", () =>
122
140
  socket.send(JSON.stringify(firstMessage)),
123
141
  );
124
142
  socket.addEventListener("message", async (event) => {
125
143
  const message = JSON.parse(String(event.data));
126
144
  if (message.type === "paired") {
145
+ clearConnectionWatchdog?.();
127
146
  config = {
128
147
  ...config,
129
148
  connector_id: message.connector.id,
@@ -142,6 +161,7 @@ async function connect() {
142
161
  return;
143
162
  }
144
163
  if (message.type === "authenticated") {
164
+ clearConnectionWatchdog?.();
145
165
  console.log("Connected and waiting for EngineerOS runs.");
146
166
  reconnectDelay = 1_000;
147
167
  startPings();
@@ -220,6 +240,7 @@ async function connect() {
220
240
  }
221
241
  });
222
242
  socket.addEventListener("close", () => {
243
+ clearConnectionWatchdog?.();
223
244
  clearInterval(pingTimer);
224
245
  if (active?.kind === "prompt") {
225
246
  active.cancelled = true;
@@ -233,13 +254,27 @@ async function connect() {
233
254
  return;
234
255
  }
235
256
  if (stopped) return;
257
+ scheduleReconnect();
258
+ });
259
+ socket.addEventListener("error", (event) => {
260
+ lastConnectionError = describeWebSocketError(event);
236
261
  console.error(
237
- `Connection lost. Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
262
+ `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
238
263
  );
239
- setTimeout(connect, reconnectDelay);
240
- reconnectDelay = Math.min(30_000, reconnectDelay * 2);
241
264
  });
242
- socket.addEventListener("error", () => {});
265
+ }
266
+
267
+ function scheduleReconnect() {
268
+ if (stopped || connectionRejected || reconnectTimer) return;
269
+ const detail = lastConnectionError ? ` Last error: ${lastConnectionError}` : "";
270
+ console.error(
271
+ `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
272
+ );
273
+ reconnectTimer = setTimeout(() => {
274
+ reconnectTimer = undefined;
275
+ void connect();
276
+ }, reconnectDelay);
277
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
243
278
  }
244
279
 
245
280
  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.1",
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/connection.mjs && node --check ./src/runner.mjs"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=22"
@@ -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
+ }