@engineeros/connector 0.13.4 → 0.14.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.
@@ -1,913 +1,968 @@
1
- #!/usr/bin/env node
2
- import os from "node:os";
3
- import path from "node:path";
4
- import {
5
- assessmentResultUrl,
6
- connectorResumeCredentials,
7
- loadConfig,
8
- mergePairedConfig,
9
- resultUrl,
10
- saveConfig,
11
- socketUrl,
12
- workspaceUrl,
13
- } from "../src/config.mjs";
14
- import {
15
- assessmentInactivityFailure,
16
- assessmentProgressMessage,
17
- assessmentStreamDelta,
18
- applyAcceptedChange,
19
- enqueueWorkspaceAssessment,
20
- executeAssignment,
21
- executeConnectedPrompt,
22
- executeWorkspaceAssessment,
1
+ #!/usr/bin/env node
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ assessmentResultUrl,
6
+ connectorResumeCredentials,
7
+ loadConfig,
8
+ mergePairedConfig,
9
+ resultUrl,
10
+ saveConfig,
11
+ socketUrl,
12
+ workspaceUrl,
13
+ } from "../src/config.mjs";
14
+ import {
15
+ assessmentInactivityFailure,
16
+ assessmentProgressMessage,
17
+ assessmentStreamDelta,
18
+ applyAcceptedChange,
19
+ enqueueWorkspaceAssessment,
20
+ executeAssignment,
21
+ executeConnectedPrompt,
22
+ executeWorkspaceAssessment,
23
23
  inspectCodingAgent,
24
24
  promptStreamEvent,
25
25
  requeueInterruptedAssessment,
26
+ shouldReportAssessmentProgress,
26
27
  stopProcess,
28
+ submitAssessmentResultWithRetry,
27
29
  takeWorkspaceAssessmentWave,
30
+ workspaceAssessmentWorkerLimit,
28
31
  workspaceSnapshot,
29
- } from "../src/runner.mjs";
30
- import { disposeAcpRuntimes } from "../src/acp-client.mjs";
31
- import {
32
- installRegisteredAgent,
33
- inspectRegisteredAgent,
34
- registeredAgentConfig,
35
- registeredAgents,
36
- } from "../src/agent-registry.mjs";
37
- import {
38
- describeRejectedResponse,
39
- describeWebSocketError,
40
- protocolFailureMessage,
41
- startConnectionWatchdog,
42
- } from "../src/connection.mjs";
43
- import { advertisedCapabilities } from "../src/capabilities.mjs";
44
- import { parseConnectorArgs } from "../src/cli-args.mjs";
45
- import { runMcpServer } from "../src/mcp-server.mjs";
46
- import packageJson from "../package.json" with { type: "json" };
47
-
48
- const { command, positional, flags } = parseConnectorArgs(
49
- process.argv.slice(2),
32
+ } from "../src/runner.mjs";
33
+ import { disposeAcpRuntimes } from "../src/acp-client.mjs";
34
+ import {
35
+ installRegisteredAgent,
36
+ inspectRegisteredAgent,
37
+ registeredAgentConfig,
38
+ registeredAgents,
39
+ } from "../src/agent-registry.mjs";
40
+ import {
41
+ describeRejectedResponse,
42
+ describeWebSocketError,
43
+ protocolFailureMessage,
44
+ startConnectionWatchdog,
45
+ } from "../src/connection.mjs";
46
+ import { advertisedCapabilities } from "../src/capabilities.mjs";
47
+ import { parseConnectorArgs } from "../src/cli-args.mjs";
48
+ import { runMcpServer } from "../src/mcp-server.mjs";
49
+ import packageJson from "../package.json" with { type: "json" };
50
+
51
+ const { command, positional, flags } = parseConnectorArgs(
52
+ process.argv.slice(2),
53
+ );
54
+
55
+ if (command === "agents") {
56
+ let agents;
57
+ try {
58
+ agents = await registeredAgents();
59
+ } catch (error) {
60
+ fail(error instanceof Error ? error.message : String(error));
61
+ }
62
+ const statuses = await Promise.all(
63
+ agents.map(async (agent) => {
64
+ try {
65
+ await inspectRegisteredAgent(agent.id);
66
+ return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
67
+ } catch (error) {
68
+ const detail = error instanceof Error ? error.message : String(error);
69
+ return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
70
+ }
71
+ }),
72
+ );
73
+ for (const status of statuses) {
74
+ console.log(status);
75
+ }
76
+ process.exit(0);
77
+ }
78
+
79
+ if (command === "agent") {
80
+ const agentId = positional[0];
81
+ const action = positional[1] || "check";
82
+ if (!agentId || !["check", "install"].includes(action)) {
83
+ fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
84
+ }
85
+ try {
86
+ const installed =
87
+ action === "install"
88
+ ? await installRegisteredAgent(agentId)
89
+ : await inspectRegisteredAgent(agentId);
90
+ console.log(
91
+ `${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
92
+ );
93
+ } catch (error) {
94
+ fail(error instanceof Error ? error.message : String(error));
95
+ }
96
+ process.exit(0);
97
+ }
98
+
99
+ if (command === "mcp") {
100
+ const config = await loadConfig(flags.workspace || process.cwd());
101
+ if (!config) fail("This workspace is not paired with EngineerOS.");
102
+ const runId = flags["run-id"] || positional[0];
103
+ if (!runId)
104
+ fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
105
+ await runMcpServer({
106
+ config,
107
+ runId,
108
+ version: packageJson.version,
109
+ input: process.stdin,
110
+ output: process.stdout,
111
+ });
112
+ process.exit(0);
113
+ }
114
+
115
+ if (command === "status") {
116
+ const config = await loadConfig(flags.workspace || process.cwd());
117
+ console.log(
118
+ config
119
+ ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
120
+ : "Not paired",
121
+ );
122
+ process.exit(config ? 0 : 1);
123
+ }
124
+
125
+ let config;
126
+ let firstMessage;
127
+ let existingConfig;
128
+ if (command === "pair") {
129
+ const pairingCode = positional[0];
130
+ if (!pairingCode)
131
+ fail(
132
+ "Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
133
+ );
134
+ const url = flags.url;
135
+ if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
136
+ if (flags.agent && flags["agent-command"]) {
137
+ fail("Use either --agent or --agent-command, not both.");
138
+ }
139
+ let agentConfig = {};
140
+ try {
141
+ if (flags.agent) {
142
+ await inspectRegisteredAgent(flags.agent);
143
+ agentConfig = await registeredAgentConfig(flags.agent);
144
+ }
145
+ } catch (error) {
146
+ fail(error instanceof Error ? error.message : String(error));
147
+ }
148
+ config = {
149
+ server_url: socketUrl(url),
150
+ workspace: path.resolve(flags.workspace || process.cwd()),
151
+ onboard: flags.onboard === true,
152
+ onboarding_pending: flags.onboard === true,
153
+ agent_protocol: flags["agent-command"] ? "acp" : "codex",
154
+ agent_command: flags["agent-command"] || null,
155
+ agent_args: parseAgentArgs(flags["agent-args"]),
156
+ agent_name: flags["agent-name"] || null,
157
+ ...agentConfig,
158
+ skip_git_repo_check: flags["skip-git-repo-check"] === true,
159
+ name:
160
+ flags.name ||
161
+ `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
162
+ };
163
+ existingConfig = await loadConfig(config.workspace);
164
+ firstMessage = {
165
+ type: "pair",
166
+ pairing_code: pairingCode,
167
+ name: config.name,
168
+ capabilities: {},
169
+ ...connectorResumeCredentials(existingConfig, config.server_url),
170
+ };
171
+ } else if (command === "start") {
172
+ config = await loadConfig(flags.workspace || process.cwd());
173
+ if (!config)
174
+ fail(
175
+ "This connector is not paired. Create a pairing command in EngineerOS first.",
176
+ );
177
+ firstMessage = {
178
+ type: "authenticate",
179
+ connector_id: config.connector_id,
180
+ token: config.token,
181
+ };
182
+ } else {
183
+ fail(
184
+ "Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
185
+ );
186
+ }
187
+
188
+ let codingAgent;
189
+ try {
190
+ codingAgent = await inspectCodingAgent(config, config.workspace);
191
+ } catch (error) {
192
+ fail(error instanceof Error ? error.message : String(error));
193
+ }
194
+ console.log(
195
+ `Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
196
+ );
197
+ const capabilities = advertisedCapabilities(config, codingAgent);
198
+ firstMessage.capabilities = capabilities;
199
+
200
+ let stopped = false;
201
+ let active = null;
202
+ const activeAssessments = new Map();
203
+ const ASSESSMENT_WORKER_LIMIT = workspaceAssessmentWorkerLimit(
204
+ codingAgent.protocol,
50
205
  );
51
-
52
- if (command === "agents") {
53
- let agents;
54
- try {
55
- agents = await registeredAgents();
56
- } catch (error) {
57
- fail(error instanceof Error ? error.message : String(error));
58
- }
59
- const statuses = await Promise.all(
60
- agents.map(async (agent) => {
61
- try {
62
- await inspectRegisteredAgent(agent.id);
63
- return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
64
- } catch (error) {
65
- const detail = error instanceof Error ? error.message : String(error);
66
- return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
67
- }
68
- }),
69
- );
70
- for (const status of statuses) {
71
- console.log(status);
72
- }
73
- process.exit(0);
74
- }
75
-
76
- if (command === "agent") {
77
- const agentId = positional[0];
78
- const action = positional[1] || "check";
79
- if (!agentId || !["check", "install"].includes(action)) {
80
- fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
81
- }
82
- try {
83
- const installed =
84
- action === "install"
85
- ? await installRegisteredAgent(agentId)
86
- : await inspectRegisteredAgent(agentId);
87
- console.log(
88
- `${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
89
- );
90
- } catch (error) {
91
- fail(error instanceof Error ? error.message : String(error));
92
- }
93
- process.exit(0);
94
- }
95
-
96
- if (command === "mcp") {
97
- const config = await loadConfig(flags.workspace || process.cwd());
98
- if (!config) fail("This workspace is not paired with EngineerOS.");
99
- const runId = flags["run-id"] || positional[0];
100
- if (!runId)
101
- fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
102
- await runMcpServer({
103
- config,
104
- runId,
105
- version: packageJson.version,
106
- input: process.stdin,
107
- output: process.stdout,
108
- });
109
- process.exit(0);
110
- }
111
-
112
- if (command === "status") {
113
- const config = await loadConfig(flags.workspace || process.cwd());
206
+ if (ASSESSMENT_WORKER_LIMIT === 1) {
114
207
  console.log(
115
- config
116
- ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
117
- : "Not paired",
118
- );
119
- process.exit(config ? 0 : 1);
120
- }
121
-
122
- let config;
123
- let firstMessage;
124
- let existingConfig;
125
- if (command === "pair") {
126
- const pairingCode = positional[0];
127
- if (!pairingCode)
128
- fail(
129
- "Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
130
- );
131
- const url = flags.url;
132
- if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
133
- if (flags.agent && flags["agent-command"]) {
134
- fail("Use either --agent or --agent-command, not both.");
135
- }
136
- let agentConfig = {};
137
- try {
138
- if (flags.agent) {
139
- await inspectRegisteredAgent(flags.agent);
140
- agentConfig = await registeredAgentConfig(flags.agent);
141
- }
142
- } catch (error) {
143
- fail(error instanceof Error ? error.message : String(error));
144
- }
145
- config = {
146
- server_url: socketUrl(url),
147
- workspace: path.resolve(flags.workspace || process.cwd()),
148
- onboard: flags.onboard === true,
149
- onboarding_pending: flags.onboard === true,
150
- agent_protocol: flags["agent-command"] ? "acp" : "codex",
151
- agent_command: flags["agent-command"] || null,
152
- agent_args: parseAgentArgs(flags["agent-args"]),
153
- agent_name: flags["agent-name"] || null,
154
- ...agentConfig,
155
- skip_git_repo_check: flags["skip-git-repo-check"] === true,
156
- name:
157
- flags.name ||
158
- `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
159
- };
160
- existingConfig = await loadConfig(config.workspace);
161
- firstMessage = {
162
- type: "pair",
163
- pairing_code: pairingCode,
164
- name: config.name,
165
- capabilities: {},
166
- ...connectorResumeCredentials(existingConfig, config.server_url),
167
- };
168
- } else if (command === "start") {
169
- config = await loadConfig(flags.workspace || process.cwd());
170
- if (!config)
171
- fail(
172
- "This connector is not paired. Create a pairing command in EngineerOS first.",
173
- );
174
- firstMessage = {
175
- type: "authenticate",
176
- connector_id: config.connector_id,
177
- token: config.token,
178
- };
179
- } else {
180
- fail(
181
- "Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
208
+ "ACP assessments run one stage at a time to keep the local agent and EngineerOS responsive.",
182
209
  );
183
210
  }
184
-
185
- let codingAgent;
186
- try {
187
- codingAgent = await inspectCodingAgent(config, config.workspace);
188
- } catch (error) {
189
- fail(error instanceof Error ? error.message : String(error));
190
- }
191
- console.log(
192
- `Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
193
- );
194
- const capabilities = advertisedCapabilities(config, codingAgent);
195
- firstMessage.capabilities = capabilities;
196
-
197
- let stopped = false;
198
- let active = null;
199
- const activeAssessments = new Map();
200
- const ASSESSMENT_WORKER_LIMIT = 3;
201
- const available = [];
202
- const assessments = [];
203
- const activePrompts = new Map();
204
- let socket;
205
- let pingTimer;
206
- let reconnectTimer;
207
- let clearConnectionWatchdog;
208
- let reconnectDelay = 1_000;
209
- let connectionRejected = false;
210
- let lastConnectionError;
211
- let snapshotInFlight = false;
212
-
213
- process.on("SIGINT", async () => {
214
- stopped = true;
215
- clearConnectionWatchdog?.();
216
- clearTimeout(reconnectTimer);
217
- clearInterval(pingTimer);
218
- await Promise.all([
219
- stopProcess(active?.child),
220
- ...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
221
- ]);
222
- await Promise.all([...activePrompts.values()].map(cancelPrompt));
223
- await disposeAcpRuntimes();
224
- socket?.close();
225
- process.exit(0);
226
- });
227
-
228
- await connect();
229
-
230
- async function connect() {
231
- console.log(`Connecting ${config.name} to ${config.server_url}`);
232
- connectionRejected = false;
233
- lastConnectionError = undefined;
234
- socket = new WebSocket(config.server_url);
235
- clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
236
- onTimeout: (message) => {
237
- lastConnectionError = message;
238
- console.error(message);
239
- scheduleReconnect();
240
- },
241
- });
242
- socket.addEventListener("open", () =>
243
- socket.send(JSON.stringify(firstMessage)),
244
- );
245
- socket.addEventListener("message", async (event) => {
246
- const message = JSON.parse(String(event.data));
247
- if (message.type === "paired") {
248
- clearConnectionWatchdog?.();
249
- const reusedConnector =
250
- existingConfig?.connector_id === message.connector.id;
251
- config = mergePairedConfig(
252
- config,
253
- existingConfig,
254
- message.connector.id,
255
- message.token,
256
- );
257
- await saveConfig(config);
258
- firstMessage = {
259
- type: "authenticate",
260
- connector_id: config.connector_id,
261
- token: config.token,
262
- capabilities,
263
- };
264
- console.log(
265
- reusedConnector
266
- ? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
267
- : `Paired. Connector ${config.connector_id} is online.`,
268
- );
269
- reconnectDelay = 1_000;
270
- startPings();
271
- if (config.onboarding_pending) void submitWorkspaceSnapshot();
272
- return;
273
- }
274
- if (message.type === "authenticated") {
275
- clearConnectionWatchdog?.();
276
- console.log("Connected and waiting for EngineerOS runs.");
277
- reconnectDelay = 1_000;
278
- startPings();
279
- if (config.onboarding_pending) void submitWorkspaceSnapshot();
280
- return;
281
- }
282
- if (message.type === "workspace.refresh") {
283
- void submitWorkspaceSnapshot();
284
- return;
285
- }
286
- if (message.type === "workspace.assessment") {
287
- const assessmentKey = `${message.assessment_id}:${message.stage}`;
288
- const disposition = enqueueWorkspaceAssessment(
289
- assessments,
290
- activeAssessments.get(assessmentKey),
291
- message,
292
- );
293
- if (disposition === "deferred") {
294
- console.log(
295
- `Assessment stage ${message.stage} is still active after reconnect; preserving the replay until it finishes.`,
296
- );
297
- }
298
- pump();
299
- return;
300
- }
301
- if (message.type === "prompt.execute") {
302
- if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
303
- return;
304
- }
305
- if (message.type === "prompt.cancel") {
306
- const promptState = activePrompts.get(message.prompt_id);
307
- if (promptState) await cancelPrompt(promptState);
308
- return;
309
- }
310
- if (message.type === "run.available") {
311
- if (!available.includes(message.run_id)) available.push(message.run_id);
312
- pump();
313
- return;
314
- }
315
- if (message.type === "run.assignment") {
316
- await execute(message);
317
- return;
318
- }
319
- if (message.type === "run.cancelled" && active?.runId === message.run_id) {
320
- console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
321
- active.cancelled = true;
322
- await stopProcess(active.child);
323
- return;
324
- }
325
- if (message.type === "connector.revoked") {
326
- stopped = true;
327
- console.error("This connector was revoked in EngineerOS.");
328
- socket.close();
329
- return;
330
- }
331
- if (message.type === "run.error") {
332
- console.error(`EngineerOS: ${message.message}`);
333
- if (!message.run_id) snapshotInFlight = false;
334
- if (active?.runId === message.run_id && !active.child) {
335
- active = null;
336
- pump();
337
- }
338
- }
339
- if (message.type === "connection.error") {
340
- connectionRejected = true;
341
- console.error(`EngineerOS: ${message.message}`);
342
- }
343
- });
344
- socket.addEventListener("close", (event) => {
345
- clearConnectionWatchdog?.();
346
- clearInterval(pingTimer);
211
+ const available = [];
212
+ const assessments = [];
213
+ const activePrompts = new Map();
214
+ let socket;
215
+ let pingTimer;
216
+ let reconnectTimer;
217
+ let clearConnectionWatchdog;
218
+ let reconnectDelay = 1_000;
219
+ let connectionRejected = false;
220
+ let lastConnectionError;
221
+ let snapshotInFlight = false;
222
+
223
+ process.on("SIGINT", async () => {
224
+ stopped = true;
225
+ clearConnectionWatchdog?.();
226
+ clearTimeout(reconnectTimer);
227
+ clearInterval(pingTimer);
228
+ await Promise.all([
229
+ stopProcess(active?.child),
230
+ ...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
231
+ ]);
232
+ await Promise.all([...activePrompts.values()].map(cancelPrompt));
233
+ await disposeAcpRuntimes();
234
+ socket?.close();
235
+ process.exit(0);
236
+ });
237
+
238
+ await connect();
239
+
240
+ async function connect() {
241
+ console.log(`Connecting ${config.name} to ${config.server_url}`);
242
+ connectionRejected = false;
243
+ lastConnectionError = undefined;
244
+ socket = new WebSocket(config.server_url);
245
+ clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
246
+ onTimeout: (message) => {
247
+ lastConnectionError = message;
248
+ console.error(message);
249
+ scheduleReconnect();
250
+ },
251
+ });
252
+ socket.addEventListener("open", () =>
253
+ socket.send(JSON.stringify(firstMessage)),
254
+ );
255
+ socket.addEventListener("message", async (event) => {
256
+ const message = JSON.parse(String(event.data));
257
+ if (message.type === "paired") {
258
+ clearConnectionWatchdog?.();
259
+ const reusedConnector =
260
+ existingConfig?.connector_id === message.connector.id;
261
+ config = mergePairedConfig(
262
+ config,
263
+ existingConfig,
264
+ message.connector.id,
265
+ message.token,
266
+ );
267
+ await saveConfig(config);
268
+ firstMessage = {
269
+ type: "authenticate",
270
+ connector_id: config.connector_id,
271
+ token: config.token,
272
+ capabilities,
273
+ };
274
+ console.log(
275
+ reusedConnector
276
+ ? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
277
+ : `Paired. Connector ${config.connector_id} is online.`,
278
+ );
279
+ reconnectDelay = 1_000;
280
+ startPings();
281
+ if (config.onboarding_pending) void submitWorkspaceSnapshot();
282
+ return;
283
+ }
284
+ if (message.type === "authenticated") {
285
+ clearConnectionWatchdog?.();
286
+ console.log("Connected and waiting for EngineerOS runs.");
287
+ reconnectDelay = 1_000;
288
+ startPings();
289
+ if (config.onboarding_pending) void submitWorkspaceSnapshot();
290
+ return;
291
+ }
292
+ if (message.type === "workspace.refresh") {
293
+ void submitWorkspaceSnapshot();
294
+ return;
295
+ }
296
+ if (message.type === "workspace.assessment") {
297
+ const assessmentKey = `${message.assessment_id}:${message.stage}`;
298
+ const disposition = enqueueWorkspaceAssessment(
299
+ assessments,
300
+ activeAssessments.get(assessmentKey),
301
+ message,
302
+ );
303
+ if (disposition === "deferred") {
304
+ console.log(
305
+ `Assessment stage ${message.stage} is still active after reconnect; preserving the replay until it finishes.`,
306
+ );
307
+ }
308
+ pump();
309
+ return;
310
+ }
311
+ if (message.type === "prompt.execute") {
312
+ if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
313
+ return;
314
+ }
315
+ if (message.type === "prompt.cancel") {
316
+ const promptState = activePrompts.get(message.prompt_id);
317
+ if (promptState) await cancelPrompt(promptState);
318
+ return;
319
+ }
320
+ if (message.type === "run.available") {
321
+ if (!available.includes(message.run_id)) available.push(message.run_id);
322
+ pump();
323
+ return;
324
+ }
325
+ if (message.type === "run.assignment") {
326
+ await execute(message);
327
+ return;
328
+ }
329
+ if (message.type === "run.cancelled" && active?.runId === message.run_id) {
330
+ console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
331
+ active.cancelled = true;
332
+ await stopProcess(active.child);
333
+ return;
334
+ }
335
+ if (message.type === "connector.revoked") {
336
+ stopped = true;
337
+ console.error("This connector was revoked in EngineerOS.");
338
+ socket.close();
339
+ return;
340
+ }
341
+ if (message.type === "run.error") {
342
+ console.error(`EngineerOS: ${message.message}`);
343
+ if (!message.run_id) snapshotInFlight = false;
344
+ if (active?.runId === message.run_id && !active.child) {
345
+ active = null;
346
+ pump();
347
+ }
348
+ }
349
+ if (message.type === "connection.error") {
350
+ connectionRejected = true;
351
+ console.error(`EngineerOS: ${message.message}`);
352
+ }
353
+ });
354
+ socket.addEventListener("close", (event) => {
355
+ clearConnectionWatchdog?.();
356
+ clearInterval(pingTimer);
347
357
  for (const promptState of activePrompts.values())
348
358
  void cancelPrompt(promptState);
349
359
  for (const assessmentState of activeAssessments.values()) {
350
- assessmentState.transportInterrupted = true;
351
360
  if (event.code === 4001) void stopProcess(assessmentState.child);
352
361
  }
353
- if (connectionRejected) {
354
- stopped = true;
355
- console.error(
356
- "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
357
- );
358
- return;
359
- }
360
- if (stopped) return;
361
- scheduleReconnect();
362
- });
363
- socket.addEventListener("error", (event) => {
364
- lastConnectionError = describeWebSocketError(event);
365
- console.error(
366
- `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
367
- );
368
- });
369
- }
370
-
371
- function scheduleReconnect() {
372
- if (stopped || connectionRejected || reconnectTimer) return;
373
- const detail = lastConnectionError
374
- ? ` Last error: ${lastConnectionError}`
375
- : "";
376
- console.error(
377
- `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
378
- );
379
- reconnectTimer = setTimeout(() => {
380
- reconnectTimer = undefined;
381
- void connect();
382
- }, reconnectDelay);
383
- reconnectDelay = Math.min(30_000, reconnectDelay * 2);
384
- }
385
-
386
- async function submitWorkspaceSnapshot() {
387
- if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
388
- snapshotInFlight = true;
389
- console.log("Inspecting the workspace without executing its code.");
390
- try {
391
- const snapshot = await workspaceSnapshot(config.workspace);
392
- const response = await fetch(
393
- workspaceUrl(config.server_url, config.connector_id),
394
- {
395
- method: "POST",
396
- headers: {
397
- "Content-Type": "application/json",
398
- Authorization: `Bearer ${config.token}`,
399
- },
400
- body: JSON.stringify(snapshot),
401
- },
402
- );
403
- if (!response.ok) {
404
- throw new Error(
405
- await describeRejectedResponse(response, "the workspace"),
406
- );
407
- }
408
- const result = await response.json();
409
- config = { ...config, onboarding_pending: false };
410
- await saveConfig(config);
411
- console.log(
412
- `Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
413
- );
414
- console.log(
415
- `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized evidence file(s).`,
416
- );
417
- console.log(
418
- `Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
419
- );
420
- if (snapshot.omitted_evidence_file_count > 0) {
421
- console.log(
422
- `${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
423
- );
424
- }
425
- const executionProfiles =
426
- result.connector?.capabilities?.execution_profiles;
427
- const canSelectAssessmentModel =
428
- executionProfiles?.model_selection === true &&
429
- Array.isArray(executionProfiles.models) &&
430
- executionProfiles.models.length > 0;
431
- console.log(
432
- canSelectAssessmentModel
433
- ? `Workspace inventory registered as ${result.workspace_kind}. Select the assessment model in EngineerOS to continue.`
434
- : `Workspace inventory registered as ${result.workspace_kind}. This agent did not advertise selectable models; update it and reconnect before starting the baseline assessment.`,
435
- );
436
- snapshotInFlight = false;
437
- } catch (error) {
438
- snapshotInFlight = false;
439
- console.error(
440
- `Workspace assessment failed: ${protocolFailureMessage(error)}`,
441
- );
442
- }
443
- }
444
-
445
- function startPings() {
446
- clearInterval(pingTimer);
447
- pingTimer = setInterval(() => {
448
- if (socket.readyState === WebSocket.OPEN) {
449
- socket.send(
450
- JSON.stringify({
451
- type: "ping",
452
- active_run_id: active?.kind === "goal" ? active.runId : null,
453
- }),
454
- );
455
- }
456
- }, 10_000);
457
- }
458
-
459
- function sendPromptEvent(promptId, event) {
460
- if (!event || socket.readyState !== WebSocket.OPEN) return;
461
- const payload = {
462
- type: "prompt.event",
463
- prompt_id: promptId,
464
- kind: event.kind,
465
- };
466
- if (event.message) payload.message = String(event.message).slice(0, 500);
467
- if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
468
- if (event.status) payload.status = String(event.status).slice(0, 100);
469
- socket.send(JSON.stringify(payload));
470
- }
471
-
472
- function pump() {
473
- if (socket.readyState !== WebSocket.OPEN || active) return;
474
- const wave = takeWorkspaceAssessmentWave(
475
- assessments,
476
- activeAssessments.size,
477
- ASSESSMENT_WORKER_LIMIT,
478
- );
479
- for (const assessment of wave) {
480
- const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
481
- const assessmentState = {
482
- kind: "assessment",
483
- key: assessmentKey,
484
- runId: assessment.assessment_id,
362
+ if (connectionRejected) {
363
+ stopped = true;
364
+ console.error(
365
+ "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
366
+ );
367
+ return;
368
+ }
369
+ if (stopped) return;
370
+ scheduleReconnect();
371
+ });
372
+ socket.addEventListener("error", (event) => {
373
+ lastConnectionError = describeWebSocketError(event);
374
+ console.error(
375
+ `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
376
+ );
377
+ });
378
+ }
379
+
380
+ function scheduleReconnect() {
381
+ if (stopped || connectionRejected || reconnectTimer) return;
382
+ const detail = lastConnectionError
383
+ ? ` Last error: ${lastConnectionError}`
384
+ : "";
385
+ console.error(
386
+ `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
387
+ );
388
+ reconnectTimer = setTimeout(() => {
389
+ reconnectTimer = undefined;
390
+ void connect();
391
+ }, reconnectDelay);
392
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
393
+ }
394
+
395
+ async function submitWorkspaceSnapshot() {
396
+ if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
397
+ snapshotInFlight = true;
398
+ console.log("Inspecting the workspace without executing its code.");
399
+ try {
400
+ const snapshot = await workspaceSnapshot(config.workspace);
401
+ const response = await fetch(
402
+ workspaceUrl(config.server_url, config.connector_id),
403
+ {
404
+ method: "POST",
405
+ headers: {
406
+ "Content-Type": "application/json",
407
+ Authorization: `Bearer ${config.token}`,
408
+ },
409
+ body: JSON.stringify(snapshot),
410
+ },
411
+ );
412
+ if (!response.ok) {
413
+ throw new Error(
414
+ await describeRejectedResponse(response, "the workspace"),
415
+ );
416
+ }
417
+ const result = await response.json();
418
+ config = { ...config, onboarding_pending: false };
419
+ await saveConfig(config);
420
+ console.log(
421
+ `Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
422
+ );
423
+ console.log(
424
+ `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
425
+ );
426
+ console.log(
427
+ `Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
428
+ );
429
+ if (snapshot.omitted_evidence_file_count > 0) {
430
+ console.log(
431
+ `${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
432
+ );
433
+ }
434
+ const executionProfiles =
435
+ result.connector?.capabilities?.execution_profiles;
436
+ const canSelectAssessmentModel =
437
+ executionProfiles?.model_selection === true &&
438
+ Array.isArray(executionProfiles.models) &&
439
+ executionProfiles.models.length > 0;
440
+ console.log(
441
+ canSelectAssessmentModel
442
+ ? `Workspace inventory registered as ${result.workspace_kind}. Select the assessment model in EngineerOS to continue.`
443
+ : `Workspace inventory registered as ${result.workspace_kind}. This agent did not advertise selectable models; update it and reconnect before starting the baseline assessment.`,
444
+ );
445
+ snapshotInFlight = false;
446
+ } catch (error) {
447
+ snapshotInFlight = false;
448
+ console.error(
449
+ `Workspace assessment failed: ${protocolFailureMessage(error)}`,
450
+ );
451
+ }
452
+ }
453
+
454
+ function startPings() {
455
+ clearInterval(pingTimer);
456
+ pingTimer = setInterval(() => {
457
+ if (socket.readyState === WebSocket.OPEN) {
458
+ socket.send(
459
+ JSON.stringify({
460
+ type: "ping",
461
+ active_run_id: active?.kind === "goal" ? active.runId : null,
462
+ }),
463
+ );
464
+ }
465
+ }, 10_000);
466
+ }
467
+
468
+ function sendPromptEvent(promptId, event) {
469
+ if (!event || socket.readyState !== WebSocket.OPEN) return;
470
+ const payload = {
471
+ type: "prompt.event",
472
+ prompt_id: promptId,
473
+ kind: event.kind,
474
+ };
475
+ if (event.message) payload.message = String(event.message).slice(0, 500);
476
+ if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
477
+ if (event.status) payload.status = String(event.status).slice(0, 100);
478
+ socket.send(JSON.stringify(payload));
479
+ }
480
+
481
+ function pump() {
482
+ if (socket.readyState !== WebSocket.OPEN || active) return;
483
+ const wave = takeWorkspaceAssessmentWave(
484
+ assessments,
485
+ activeAssessments.size,
486
+ ASSESSMENT_WORKER_LIMIT,
487
+ );
488
+ for (const assessment of wave) {
489
+ const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
490
+ const assessmentState = {
491
+ kind: "assessment",
492
+ key: assessmentKey,
493
+ runId: assessment.assessment_id,
485
494
  stage: assessment.stage,
486
495
  child: null,
487
496
  resumeAssignment: null,
488
- transportInterrupted: false,
489
497
  };
490
- activeAssessments.set(assessmentKey, assessmentState);
491
- void executeAssessment(assessment, assessmentState);
492
- }
493
- if (activeAssessments.size > 0 || assessments.length > 0) return;
494
- const runId = available.shift();
495
- if (!runId) return;
496
- active = { kind: "goal", runId, child: null, cancelled: false };
497
- socket.send(
498
- JSON.stringify({
499
- type: "run.claim",
500
- run_id: runId,
501
- runner_name: codingAgent.name,
502
- metadata: {
503
- hostname: os.hostname(),
504
- workspace_name: path.basename(config.workspace),
505
- },
506
- }),
507
- );
508
- }
509
-
510
- async function executePrompt(assignment) {
511
- const promptId = assignment.prompt_id;
512
- const promptState = {
513
- runId: promptId,
514
- child: null,
515
- controller: null,
516
- cancelled: false,
517
- };
518
- activePrompts.set(promptId, promptState);
519
- let lastEvent;
520
- const reportEvent = (event) => {
521
- const serialized = event ? JSON.stringify(event) : null;
522
- if (
523
- !serialized ||
524
- serialized === lastEvent ||
525
- socket.readyState !== WebSocket.OPEN ||
526
- activePrompts.get(promptId) !== promptState
527
- ) {
528
- return;
529
- }
530
- lastEvent = serialized;
531
- sendPromptEvent(promptId, event);
532
- };
533
- sendPromptEvent(promptId, {
534
- kind: "status",
535
- message: "Agent started this request",
536
- });
537
- console.log(
538
- `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
539
- );
540
- try {
541
- const result = await executeConnectedPrompt(assignment, config, {
542
- onProcess: (child) => {
543
- if (activePrompts.get(promptId) === promptState)
544
- promptState.child = child;
545
- },
546
- onController: (controller) => {
547
- if (activePrompts.get(promptId) === promptState) {
548
- promptState.controller = controller;
549
- }
550
- },
551
- onEvent: (event) => reportEvent(promptStreamEvent(event)),
552
- });
553
- config = {
554
- ...config,
555
- sessions: {
556
- ...(config.sessions || {}),
557
- [result.sessionKey]: result.sessionId,
558
- },
559
- };
560
- await saveConfig(config);
561
- if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
562
- socket.send(
563
- JSON.stringify({
564
- type: "prompt.completed",
565
- prompt_id: promptId,
566
- content: result.content,
567
- model: result.model,
568
- session_id: result.sessionId,
569
- usage: result.usage,
570
- }),
571
- );
572
- } catch (error) {
573
- const message = protocolFailureMessage(error);
574
- if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
575
- socket.send(
576
- JSON.stringify({
577
- type: "prompt.failed",
578
- prompt_id: promptId,
579
- message,
580
- }),
581
- );
582
- }
583
- if (!promptState.cancelled) {
584
- console.error(message);
585
- }
586
- } finally {
587
- if (activePrompts.get(promptId) === promptState)
588
- activePrompts.delete(promptId);
589
- }
590
- }
591
-
592
- async function cancelPrompt(promptState) {
593
- promptState.cancelled = true;
594
- if (typeof promptState.controller?.cancel === "function") {
595
- await promptState.controller.cancel();
596
- return;
597
- }
598
- await stopProcess(promptState.child);
599
- }
600
-
601
- async function executeAssessment(assignment, assessmentState) {
602
- const assessmentId = assignment.assessment_id;
603
- const stage = assignment.stage;
604
- console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
605
- const stageStartedAt = Date.now();
606
- let lastAgentActivityAt = stageStartedAt;
607
- let lastAgentStatus = "Starting the connected agent";
498
+ activeAssessments.set(assessmentKey, assessmentState);
499
+ void executeAssessment(assessment, assessmentState);
500
+ }
501
+ if (activeAssessments.size > 0 || assessments.length > 0) return;
502
+ const runId = available.shift();
503
+ if (!runId) return;
504
+ active = { kind: "goal", runId, child: null, cancelled: false };
505
+ socket.send(
506
+ JSON.stringify({
507
+ type: "run.claim",
508
+ run_id: runId,
509
+ runner_name: codingAgent.name,
510
+ metadata: {
511
+ hostname: os.hostname(),
512
+ workspace_name: path.basename(config.workspace),
513
+ },
514
+ }),
515
+ );
516
+ }
517
+
518
+ async function executePrompt(assignment) {
519
+ const promptId = assignment.prompt_id;
520
+ const promptState = {
521
+ runId: promptId,
522
+ child: null,
523
+ controller: null,
524
+ cancelled: false,
525
+ };
526
+ activePrompts.set(promptId, promptState);
527
+ let lastEvent;
528
+ const reportEvent = (event) => {
529
+ const serialized = event ? JSON.stringify(event) : null;
530
+ if (
531
+ !serialized ||
532
+ serialized === lastEvent ||
533
+ socket.readyState !== WebSocket.OPEN ||
534
+ activePrompts.get(promptId) !== promptState
535
+ ) {
536
+ return;
537
+ }
538
+ lastEvent = serialized;
539
+ sendPromptEvent(promptId, event);
540
+ };
541
+ sendPromptEvent(promptId, {
542
+ kind: "status",
543
+ message: "Agent started this request",
544
+ });
545
+ console.log(
546
+ `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
547
+ );
548
+ try {
549
+ const result = await executeConnectedPrompt(assignment, config, {
550
+ onProcess: (child) => {
551
+ if (activePrompts.get(promptId) === promptState)
552
+ promptState.child = child;
553
+ },
554
+ onController: (controller) => {
555
+ if (activePrompts.get(promptId) === promptState) {
556
+ promptState.controller = controller;
557
+ }
558
+ },
559
+ onEvent: (event) => reportEvent(promptStreamEvent(event)),
560
+ });
561
+ config = {
562
+ ...config,
563
+ sessions: {
564
+ ...(config.sessions || {}),
565
+ [result.sessionKey]: result.sessionId,
566
+ },
567
+ };
568
+ await saveConfig(config);
569
+ if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
570
+ socket.send(
571
+ JSON.stringify({
572
+ type: "prompt.completed",
573
+ prompt_id: promptId,
574
+ content: result.content,
575
+ model: result.model,
576
+ session_id: result.sessionId,
577
+ usage: result.usage,
578
+ }),
579
+ );
580
+ } catch (error) {
581
+ const message = protocolFailureMessage(error);
582
+ if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
583
+ socket.send(
584
+ JSON.stringify({
585
+ type: "prompt.failed",
586
+ prompt_id: promptId,
587
+ message,
588
+ }),
589
+ );
590
+ }
591
+ if (!promptState.cancelled) {
592
+ console.error(message);
593
+ }
594
+ } finally {
595
+ if (activePrompts.get(promptId) === promptState)
596
+ activePrompts.delete(promptId);
597
+ }
598
+ }
599
+
600
+ async function cancelPrompt(promptState) {
601
+ promptState.cancelled = true;
602
+ if (typeof promptState.controller?.cancel === "function") {
603
+ await promptState.controller.cancel();
604
+ return;
605
+ }
606
+ await stopProcess(promptState.child);
607
+ }
608
+
609
+ async function executeAssessment(assignment, assessmentState) {
610
+ const assessmentId = assignment.assessment_id;
611
+ const stage = assignment.stage;
612
+ console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
613
+ const stageStartedAt = Date.now();
614
+ let lastAgentActivityAt = stageStartedAt;
615
+ let lastAgentStatus = "Starting the connected agent";
608
616
  let agentEventCount = 0;
609
617
  let agentProcessStarted = false;
610
- let statusTicks = 0;
611
- let progress = 5;
612
- let outputBuffer = "";
613
- let outputLength = 0;
614
- let outputTimer;
615
- let accepted = false;
616
- let failureReported = false;
618
+ let agentCompleted = false;
619
+ let statusTicks = 0;
620
+ let progress = 5;
621
+ let outputBuffer = "";
622
+ let outputLength = 0;
623
+ let outputTimer;
624
+ let accepted = false;
625
+ let failureReported = false;
617
626
  let inactivityFailure = null;
618
627
  let lastReportedMilestone = null;
628
+ let lastProgressMessage = null;
629
+ let lastProgressPercent = -1;
630
+ let lastProgressSentAt = 0;
619
631
  const creditedMilestones = new Set();
620
632
  const isActiveAssessment = () =>
621
633
  activeAssessments.get(assessmentState.key) === assessmentState;
622
- const reportProgress = (message, { milestone = true } = {}) => {
623
- if (!isActiveAssessment()) return;
624
- if (milestone && lastReportedMilestone === message) return;
625
- if (milestone) {
626
- lastReportedMilestone = message;
627
- if (!creditedMilestones.has(message)) {
628
- creditedMilestones.add(message);
629
- progress = Math.min(90, progress + 10);
634
+ const sendAssessmentMessage = (payload) => {
635
+ if (socket.readyState !== WebSocket.OPEN) return false;
636
+ try {
637
+ socket.send(JSON.stringify(payload));
638
+ return true;
639
+ } catch (error) {
640
+ lastConnectionError = protocolFailureMessage(error);
641
+ try {
642
+ socket.close();
643
+ } catch {
644
+ // The reconnect timer below owns recovery.
630
645
  }
631
- lastAgentStatus = message;
632
- console.log(`[assessment:${stage}] ${message}.`);
646
+ scheduleReconnect();
647
+ return false;
633
648
  }
634
- if (socket.readyState !== WebSocket.OPEN) return;
635
- socket.send(
636
- JSON.stringify({
649
+ };
650
+ const reportProgress = (message, { milestone = true } = {}) => {
651
+ if (!isActiveAssessment()) return;
652
+ if (milestone && lastReportedMilestone === message) return;
653
+ if (milestone) {
654
+ lastReportedMilestone = message;
655
+ if (!creditedMilestones.has(message)) {
656
+ creditedMilestones.add(message);
657
+ progress = Math.min(90, progress + 10);
658
+ }
659
+ lastAgentStatus = message;
660
+ console.log(`[assessment:${stage}] ${message}.`);
661
+ }
662
+ const boundedMessage = String(message).slice(0, 500);
663
+ const now = Date.now();
664
+ if (
665
+ !shouldReportAssessmentProgress(
666
+ {
667
+ message: boundedMessage,
668
+ progressPercent: progress,
669
+ lastMessage: lastProgressMessage,
670
+ lastProgressPercent,
671
+ lastSentAt: lastProgressSentAt,
672
+ },
673
+ { now },
674
+ )
675
+ ) {
676
+ return;
677
+ }
678
+ if (
679
+ sendAssessmentMessage({
637
680
  type: "workspace.assessment.progress",
638
681
  assessment_id: assessmentId,
639
682
  stage,
640
683
  progress_percent: progress,
641
- message: String(message).slice(0, 500),
642
- }),
643
- );
644
- };
645
- const flushOutput = () => {
646
- if (outputTimer) clearTimeout(outputTimer);
647
- outputTimer = undefined;
648
- const output = outputBuffer;
649
- outputBuffer = "";
650
- if (
651
- !output ||
652
- socket.readyState !== WebSocket.OPEN ||
653
- !isActiveAssessment()
684
+ message: boundedMessage,
685
+ })
654
686
  ) {
655
- return;
687
+ lastProgressMessage = boundedMessage;
688
+ lastProgressPercent = progress;
689
+ lastProgressSentAt = now;
690
+ }
691
+ };
692
+ const flushOutput = () => {
693
+ if (outputTimer) clearTimeout(outputTimer);
694
+ outputTimer = undefined;
695
+ const output = outputBuffer;
696
+ outputBuffer = "";
697
+ if (
698
+ !output ||
699
+ socket.readyState !== WebSocket.OPEN ||
700
+ !isActiveAssessment()
701
+ ) {
702
+ return;
656
703
  }
657
704
  for (let offset = 0; offset < output.length; offset += 50_000) {
658
- socket.send(
659
- JSON.stringify({
660
- type: "workspace.assessment.output",
661
- assessment_id: assessmentId,
662
- stage,
663
- delta: output.slice(offset, offset + 50_000),
664
- }),
665
- );
666
- }
667
- };
668
- const reportOutput = (delta) => {
669
- const remaining = 120_000 - outputLength;
670
- if (!delta || remaining <= 0) return;
671
- const bounded = String(delta).slice(0, remaining);
672
- outputBuffer += bounded;
673
- outputLength += bounded.length;
674
- if (outputBuffer.length >= 50_000) {
675
- flushOutput();
676
- } else if (!outputTimer) {
677
- outputTimer = setTimeout(flushOutput, 500);
678
- }
679
- };
680
- reportProgress(`Starting ${stage} assessment stage`);
681
- const heartbeat = setInterval(() => {
682
- const now = Date.now();
683
- const inactiveMs = now - lastAgentActivityAt;
684
- const nextInactivityFailure = agentProcessStarted
705
+ sendAssessmentMessage({
706
+ type: "workspace.assessment.output",
707
+ assessment_id: assessmentId,
708
+ stage,
709
+ delta: output.slice(offset, offset + 50_000),
710
+ });
711
+ }
712
+ };
713
+ const reportOutput = (delta) => {
714
+ const remaining = 120_000 - outputLength;
715
+ if (!delta || remaining <= 0) return;
716
+ const bounded = String(delta).slice(0, remaining);
717
+ outputBuffer += bounded;
718
+ outputLength += bounded.length;
719
+ if (outputBuffer.length >= 50_000) {
720
+ flushOutput();
721
+ } else if (!outputTimer) {
722
+ outputTimer = setTimeout(flushOutput, 500);
723
+ }
724
+ };
725
+ reportProgress(`Starting ${stage} assessment stage`);
726
+ const heartbeat = setInterval(() => {
727
+ const now = Date.now();
728
+ const inactiveMs = now - lastAgentActivityAt;
729
+ const nextInactivityFailure = agentProcessStarted && !agentCompleted
685
730
  ? assessmentInactivityFailure(stage, inactiveMs)
686
731
  : null;
687
- if (!inactivityFailure && nextInactivityFailure) {
688
- inactivityFailure = nextInactivityFailure;
689
- console.error(`[assessment:${stage}] ${inactivityFailure}`);
690
- void stopProcess(assessmentState.child);
691
- return;
692
- }
693
- if (inactivityFailure) return;
694
- reportProgress("Assessment in progress", { milestone: false });
695
- statusTicks += 1;
696
- if (statusTicks % 2 === 0) {
697
- console.log(
698
- `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
699
- `${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
700
- `(${agentEventCount.toLocaleString()} events).`,
701
- );
702
- }
703
- }, 15_000);
704
- try {
732
+ if (!inactivityFailure && nextInactivityFailure) {
733
+ inactivityFailure = nextInactivityFailure;
734
+ console.error(`[assessment:${stage}] ${inactivityFailure}`);
735
+ void stopProcess(assessmentState.child);
736
+ return;
737
+ }
738
+ if (inactivityFailure) return;
739
+ reportProgress("Assessment in progress", { milestone: false });
740
+ statusTicks += 1;
741
+ if (statusTicks % 2 === 0) {
742
+ console.log(
743
+ `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
744
+ `${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
745
+ `(${agentEventCount.toLocaleString()} events).`,
746
+ );
747
+ }
748
+ }, 15_000);
749
+ try {
705
750
  if (assignment.assessment_mode === "incremental") {
706
751
  progress = 15;
707
- reportProgress("Calculating changed files and affected evidence");
708
- }
752
+ reportProgress("Calculating changed files and affected behavior");
753
+ }
709
754
  let result = await executeWorkspaceAssessment(assignment, config, {
710
- onProcess: (child) => {
711
- if (isActiveAssessment()) {
712
- assessmentState.child = child;
713
- agentProcessStarted = true;
714
- lastAgentActivityAt = Date.now();
715
- reportProgress("Connected agent process started");
716
- }
717
- },
718
- onEvent: (event) => {
719
- lastAgentActivityAt = Date.now();
720
- agentEventCount += 1;
721
- reportOutput(assessmentStreamDelta(event));
722
- const message = assessmentProgressMessage(event);
755
+ onProcess: (child) => {
756
+ if (isActiveAssessment()) {
757
+ assessmentState.child = child;
758
+ agentProcessStarted = true;
759
+ lastAgentActivityAt = Date.now();
760
+ reportProgress("Connected agent process started");
761
+ }
762
+ },
763
+ onEvent: (event) => {
764
+ lastAgentActivityAt = Date.now();
765
+ agentEventCount += 1;
766
+ reportOutput(assessmentStreamDelta(event));
767
+ const message = assessmentProgressMessage(event);
723
768
  if (message) reportProgress(message);
724
769
  },
725
770
  });
771
+ agentCompleted = true;
726
772
  flushOutput();
727
773
  const submitResult = (payload) =>
728
774
  fetch(
729
- assessmentResultUrl(
730
- config.server_url,
731
- config.connector_id,
732
- assessmentId,
733
- ),
734
- {
735
- method: "POST",
775
+ assessmentResultUrl(
776
+ config.server_url,
777
+ config.connector_id,
778
+ assessmentId,
779
+ ),
780
+ {
781
+ method: "POST",
736
782
  headers: {
737
783
  "Content-Type": "application/json",
738
784
  Authorization: `Bearer ${config.token}`,
739
785
  },
740
786
  body: JSON.stringify(payload),
787
+ signal: AbortSignal.timeout(30_000),
741
788
  },
742
789
  );
743
- let response = await submitResult(result);
790
+ const deliverResult = (payload) =>
791
+ submitAssessmentResultWithRetry(payload, submitResult, {
792
+ isActive: () => isActiveAssessment() && !stopped,
793
+ onRetry: (error, delayMs) => {
794
+ reportProgress("Completed result is waiting for EngineerOS", {
795
+ milestone: false,
796
+ });
797
+ console.warn(
798
+ `[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
799
+ `Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
800
+ );
801
+ },
802
+ });
803
+ let response = await deliverResult(result);
744
804
  if (response.status === 422 && result.correctAfterRejection) {
745
805
  const rejection = await describeRejectedResponse(
746
806
  response,
747
807
  "the assessment",
748
808
  );
809
+ agentCompleted = false;
749
810
  result = await result.correctAfterRejection(rejection);
811
+ agentCompleted = true;
750
812
  flushOutput();
751
- response = await submitResult(result);
752
- }
753
- if (!response.ok) {
754
- throw new Error(
755
- await describeRejectedResponse(response, "the assessment"),
756
- );
757
- }
758
- accepted = true;
759
- console.log(`Workspace assessment stage ${stage} was accepted by EngineerOS.`);
760
- } catch (error) {
761
- flushOutput();
762
- const message = inactivityFailure || protocolFailureMessage(error);
763
- if (
764
- !assessmentState.transportInterrupted &&
765
- isActiveAssessment() &&
766
- socket.readyState === WebSocket.OPEN
767
- ) {
768
- socket.send(
769
- JSON.stringify({
770
- type: "workspace.assessment.failed",
771
- assessment_id: assessmentId,
772
- stage,
773
- message,
774
- }),
775
- );
776
- failureReported = true;
777
- }
778
- console.error(message);
779
- } finally {
780
- if (outputTimer) clearTimeout(outputTimer);
781
- clearInterval(heartbeat);
813
+ response = await deliverResult(result);
814
+ }
815
+ if (!response.ok) {
816
+ throw new Error(
817
+ await describeRejectedResponse(response, "the assessment"),
818
+ );
819
+ }
820
+ accepted = true;
821
+ console.log(`Workspace assessment stage ${stage} was accepted by EngineerOS.`);
822
+ } catch (error) {
823
+ flushOutput();
824
+ const message = inactivityFailure || protocolFailureMessage(error);
782
825
  if (isActiveAssessment()) {
783
- activeAssessments.delete(assessmentState.key);
784
- const replayQueued = requeueInterruptedAssessment(
785
- assessments,
786
- assessmentState,
787
- { accepted, failureReported },
788
- );
789
- if (replayQueued) {
790
- console.log(`Restarting interrupted assessment stage ${stage}.`);
791
- }
792
- pump();
793
- }
794
- }
795
- }
796
-
797
- function formatAssessmentDuration(milliseconds) {
798
- const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
799
- const minutes = Math.floor(totalSeconds / 60);
800
- const seconds = totalSeconds % 60;
801
- return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
802
- }
803
-
804
- async function execute(assignment) {
805
- const runId = assignment.run_id;
806
- console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
807
- let progress = 10;
808
- const heartbeat = setInterval(() => {
809
- if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
810
- progress = Math.min(90, progress + 5);
811
- socket.send(
812
- JSON.stringify({
813
- type: "run.progress",
814
- run_id: runId,
815
- progress_percent: progress,
816
- message: "Agent is working",
817
- }),
818
- );
819
- }
820
- }, 15_000);
821
- try {
822
- const result = await executeAssignment(assignment, config, {
823
- onProcess: (child) => {
824
- if (active?.runId === runId) active.child = child;
825
- },
826
- onEvent: (event) => {
827
- const message =
828
- event.message || event.item?.text || event.type || "Agent is working";
829
- if (socket.readyState === WebSocket.OPEN) {
830
- socket.send(
831
- JSON.stringify({
832
- type: "run.progress",
833
- run_id: runId,
834
- progress_percent: progress,
835
- message: String(message).slice(0, 500),
836
- }),
837
- );
838
- }
839
- },
840
- });
841
- if (active?.cancelled) return;
842
- const response = await fetch(
843
- resultUrl(config.server_url, config.connector_id, runId),
844
- {
845
- method: "POST",
846
- headers: {
847
- "Content-Type": "application/json",
848
- Authorization: `Bearer ${config.token}`,
849
- },
850
- body: JSON.stringify(result),
851
- },
852
- );
853
- if (!response.ok) {
854
- throw new Error(await describeRejectedResponse(response, "the result"));
855
- }
856
- const integration = await applyAcceptedChange(
857
- config.workspace,
858
- assignment.base_revision,
859
- result.head_revision,
860
- );
861
- if (integration.applied) {
862
- console.log(
863
- `Run accepted and applied to the connected repository at ${integration.revision}.`,
864
- );
865
- } else {
866
- console.warn(
867
- `Run accepted but not applied locally. ${integration.reason}`,
868
- );
869
- console.warn(
870
- `The verified run workspace remains at ${result.run_workspace}.`,
871
- );
872
- }
873
- } catch (error) {
874
- const message = protocolFailureMessage(error);
875
- if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
876
- socket.send(
877
- JSON.stringify({
878
- type: "run.failed",
879
- run_id: runId,
880
- message,
881
- }),
882
- );
883
- }
884
- if (!active?.cancelled) console.error(message);
885
- } finally {
886
- clearInterval(heartbeat);
887
- active = null;
888
- pump();
889
- }
890
- }
891
-
892
- function parseAgentArgs(value) {
893
- if (!value) return [];
894
- try {
895
- const parsed = JSON.parse(value);
896
- if (
897
- !Array.isArray(parsed) ||
898
- parsed.some((item) => typeof item !== "string")
899
- ) {
900
- throw new Error();
901
- }
902
- return parsed;
903
- } catch {
904
- fail(
905
- '--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
906
- );
907
- }
908
- }
909
-
910
- function fail(message) {
911
- console.error(message);
912
- process.exit(1);
913
- }
826
+ failureReported = sendAssessmentMessage({
827
+ type: "workspace.assessment.failed",
828
+ assessment_id: assessmentId,
829
+ stage,
830
+ message,
831
+ });
832
+ }
833
+ console.error(message);
834
+ } finally {
835
+ if (outputTimer) clearTimeout(outputTimer);
836
+ clearInterval(heartbeat);
837
+ if (isActiveAssessment()) {
838
+ activeAssessments.delete(assessmentState.key);
839
+ const replayQueued = requeueInterruptedAssessment(
840
+ assessments,
841
+ assessmentState,
842
+ { accepted, failureReported },
843
+ );
844
+ if (replayQueued) {
845
+ console.log(`Restarting interrupted assessment stage ${stage}.`);
846
+ }
847
+ pump();
848
+ }
849
+ }
850
+ }
851
+
852
+ function formatAssessmentDuration(milliseconds) {
853
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
854
+ const minutes = Math.floor(totalSeconds / 60);
855
+ const seconds = totalSeconds % 60;
856
+ return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
857
+ }
858
+
859
+ async function execute(assignment) {
860
+ const runId = assignment.run_id;
861
+ console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
862
+ let progress = 10;
863
+ const heartbeat = setInterval(() => {
864
+ if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
865
+ progress = Math.min(90, progress + 5);
866
+ socket.send(
867
+ JSON.stringify({
868
+ type: "run.progress",
869
+ run_id: runId,
870
+ progress_percent: progress,
871
+ message: "Agent is working",
872
+ }),
873
+ );
874
+ }
875
+ }, 15_000);
876
+ try {
877
+ const result = await executeAssignment(assignment, config, {
878
+ onProcess: (child) => {
879
+ if (active?.runId === runId) active.child = child;
880
+ },
881
+ onEvent: (event) => {
882
+ const message =
883
+ event.message || event.item?.text || event.type || "Agent is working";
884
+ if (socket.readyState === WebSocket.OPEN) {
885
+ socket.send(
886
+ JSON.stringify({
887
+ type: "run.progress",
888
+ run_id: runId,
889
+ progress_percent: progress,
890
+ message: String(message).slice(0, 500),
891
+ }),
892
+ );
893
+ }
894
+ },
895
+ });
896
+ if (active?.cancelled) return;
897
+ const response = await fetch(
898
+ resultUrl(config.server_url, config.connector_id, runId),
899
+ {
900
+ method: "POST",
901
+ headers: {
902
+ "Content-Type": "application/json",
903
+ Authorization: `Bearer ${config.token}`,
904
+ },
905
+ body: JSON.stringify(result),
906
+ },
907
+ );
908
+ if (!response.ok) {
909
+ throw new Error(await describeRejectedResponse(response, "the result"));
910
+ }
911
+ const integration = await applyAcceptedChange(
912
+ config.workspace,
913
+ assignment.base_revision,
914
+ result.head_revision,
915
+ );
916
+ if (integration.applied) {
917
+ console.log(
918
+ `Run accepted and applied to the connected repository at ${integration.revision}.`,
919
+ );
920
+ } else {
921
+ console.warn(
922
+ `Run accepted but not applied locally. ${integration.reason}`,
923
+ );
924
+ console.warn(
925
+ `The verified run workspace remains at ${result.run_workspace}.`,
926
+ );
927
+ }
928
+ } catch (error) {
929
+ const message = protocolFailureMessage(error);
930
+ if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
931
+ socket.send(
932
+ JSON.stringify({
933
+ type: "run.failed",
934
+ run_id: runId,
935
+ message,
936
+ }),
937
+ );
938
+ }
939
+ if (!active?.cancelled) console.error(message);
940
+ } finally {
941
+ clearInterval(heartbeat);
942
+ active = null;
943
+ pump();
944
+ }
945
+ }
946
+
947
+ function parseAgentArgs(value) {
948
+ if (!value) return [];
949
+ try {
950
+ const parsed = JSON.parse(value);
951
+ if (
952
+ !Array.isArray(parsed) ||
953
+ parsed.some((item) => typeof item !== "string")
954
+ ) {
955
+ throw new Error();
956
+ }
957
+ return parsed;
958
+ } catch {
959
+ fail(
960
+ '--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
961
+ );
962
+ }
963
+ }
964
+
965
+ function fail(message) {
966
+ console.error(message);
967
+ process.exit(1);
968
+ }