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