@engineeros/connector 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/engineeros-connector.mjs +189 -96
- package/package.json +1 -1
- package/src/acp-client.mjs +9 -8
- package/src/assessment-spool.mjs +241 -43
- package/src/codex-app-server.mjs +3 -2
- package/src/connection.mjs +8 -2
- package/src/runner.mjs +77 -31
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://your-engi
|
|
|
47
47
|
|
|
48
48
|
The connector uploads a bounded ZIP snapshot for a safe file inventory, then stays online for deep assessments, rescans, and Goal Runs. Inventory never executes repository code. It excludes known secrets, dependency directories, build output, compiled binaries, files larger than 5 MB, agent-tool caches, Git metadata, and connector state before upload.
|
|
49
49
|
|
|
50
|
-
From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select any combination of fifteen brownfield assessment domains, collectively covering 133
|
|
50
|
+
From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select any combination of fifteen brownfield assessment domains, collectively covering 133 internal checks from product and architecture through security, privacy, project-specific compliance, delivery, reliability, governance, and modernization planning. Reports include only material conclusions and actions; routine healthy checklist rows are not published. Capability entries state what they own, what they link to, and what they depend on. Connector `0.16.1` starts assessments with three parallel workers and accepts the run's UI-selected limit from three through eight for every coding-agent provider. It keeps one local assessment area at `.engineeros/assessment`: the initial accepted run creates `assessment.md`, later code revisions create one consolidated file under `changes/`, and in-progress reports plus resumable agent-session checkpoints stay under `.work/` until final acceptance. ACP agents such as OpenCode use isolated stage sessions within one persistent coding-agent process. The connector owns the worker pool and sends one aggregate heartbeat containing every active worker's phase, progress, last activity, and event count; report content is no longer streamed over the WebSocket or written into backend assessment JSON. The backend validates stage checkpoints so dependency fan-out remains authoritative, while the connector retains completed reports and reloads interrupted ACP or Codex sessions after reconnect when the coding agent supports session restoration. Transient ACP, app-server, provider-availability, and rate-limit failures receive two bounded attempts to continue the saved session without repeating completed inspection. A changed Git commit updates the same durable assessment, retains the prior commit in assessment history, and updates published artifacts in place. Connector reports and session checkpoints are tagged with their target commit so an earlier revision cannot leak into the update. If no assessment exists, EngineerOS creates the initial assessment identity. Capability detail and project-specific compliance controls fan out after their catalogs. Final synthesis reads the connector-local Markdown files, then one completed bundle is revalidated, persisted, published as canonical artifacts, and archived locally. Compliance findings describe repository-verifiable engineering readiness and missing external context, never legal compliance or certification. The connector prints each worker's safe activity and elapsed time every 30 seconds, stops a worker that produces no agent activity for ten minutes instead of waiting indefinitely, and applies separate bounded structure, semantic, and corrected-response formatting repairs before failing a stage. Assessment cannot modify tracked workspace source.
|
|
51
51
|
|
|
52
52
|
Every backend `422` content-validation response is returned to the stage agent with the exact failed item. Distinct validation failures continue through correction and resubmission until the complete stage is accepted. Three repetitions of the same unresolved validation error stop the automatic loop while preserving the latest local report for retry. Authentication, cancellation, source drift, and transport failures remain operational errors rather than agent-correction prompts.
|
|
53
53
|
|
|
@@ -21,10 +21,11 @@ import {
|
|
|
21
21
|
applyAcceptedChange,
|
|
22
22
|
enqueueWorkspaceAssessment,
|
|
23
23
|
executeAssignment,
|
|
24
|
-
executeConnectedPrompt,
|
|
25
|
-
executeWorkspaceAssessment,
|
|
26
|
-
inspectCodingAgent,
|
|
27
|
-
|
|
24
|
+
executeConnectedPrompt,
|
|
25
|
+
executeWorkspaceAssessment,
|
|
26
|
+
inspectCodingAgent,
|
|
27
|
+
isRecoverableAssessmentAgentFailure,
|
|
28
|
+
promptStreamEvent,
|
|
28
29
|
requeueInterruptedAssessment,
|
|
29
30
|
stopProcess,
|
|
30
31
|
submitAssessmentResultWithRetry,
|
|
@@ -33,12 +34,15 @@ import {
|
|
|
33
34
|
workspaceAssessmentWorkerLimit,
|
|
34
35
|
workspaceSnapshot,
|
|
35
36
|
} from "../src/runner.mjs";
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
import {
|
|
38
|
+
archiveCompletedAssessment,
|
|
39
|
+
assessmentCheckpointPayload,
|
|
40
|
+
assessmentCompletionBundle,
|
|
41
|
+
assessmentRunDirectory,
|
|
42
|
+
loadAssessmentStageSession,
|
|
43
|
+
loadAssessmentStageResult,
|
|
44
|
+
persistAssessmentStageResult,
|
|
45
|
+
persistAssessmentStageSession,
|
|
42
46
|
prepareLocalAssessmentAssignment,
|
|
43
47
|
removeAssessmentStageResult,
|
|
44
48
|
} from "../src/assessment-spool.mjs";
|
|
@@ -50,11 +54,12 @@ import {
|
|
|
50
54
|
registeredAgents,
|
|
51
55
|
} from "../src/agent-registry.mjs";
|
|
52
56
|
import {
|
|
53
|
-
describeRejectedResponse,
|
|
54
|
-
describeWebSocketError,
|
|
55
|
-
protocolFailureMessage,
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
describeRejectedResponse,
|
|
58
|
+
describeWebSocketError,
|
|
59
|
+
protocolFailureMessage,
|
|
60
|
+
sendConnectionMessage,
|
|
61
|
+
startConnectionWatchdog,
|
|
62
|
+
} from "../src/connection.mjs";
|
|
58
63
|
import { advertisedCapabilities } from "../src/capabilities.mjs";
|
|
59
64
|
import { parseConnectorArgs } from "../src/cli-args.mjs";
|
|
60
65
|
import { runMcpServer } from "../src/mcp-server.mjs";
|
|
@@ -250,25 +255,47 @@ process.on("SIGINT", async () => {
|
|
|
250
255
|
|
|
251
256
|
await connect();
|
|
252
257
|
|
|
253
|
-
async function connect() {
|
|
254
|
-
console.log(`Connecting ${config.name} to ${config.server_url}`);
|
|
255
|
-
connectionRejected = false;
|
|
256
|
-
lastConnectionError = undefined;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
258
|
+
async function connect() {
|
|
259
|
+
console.log(`Connecting ${config.name} to ${config.server_url}`);
|
|
260
|
+
connectionRejected = false;
|
|
261
|
+
lastConnectionError = undefined;
|
|
262
|
+
const connection = new WebSocket(config.server_url);
|
|
263
|
+
socket = connection;
|
|
264
|
+
const clearWatchdog = startConnectionWatchdog(connection, config.server_url, {
|
|
265
|
+
onTimeout: (message) => {
|
|
266
|
+
if (socket !== connection) return;
|
|
267
|
+
lastConnectionError = message;
|
|
268
|
+
console.error(message);
|
|
269
|
+
scheduleReconnect();
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
clearConnectionWatchdog = clearWatchdog;
|
|
273
|
+
connection.addEventListener("open", () => {
|
|
274
|
+
if (socket !== connection) {
|
|
275
|
+
connection.close();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
if (sendConnectionMessage(socket, connection, firstMessage)) return;
|
|
280
|
+
throw new Error("The WebSocket was not open when authentication started.");
|
|
281
|
+
} catch (error) {
|
|
282
|
+
lastConnectionError = protocolFailureMessage(error);
|
|
283
|
+
console.error(
|
|
284
|
+
`Could not authenticate the WebSocket connection: ${lastConnectionError}`,
|
|
285
|
+
);
|
|
286
|
+
try {
|
|
287
|
+
connection.close();
|
|
288
|
+
} catch {
|
|
289
|
+
// The reconnect timer below owns recovery.
|
|
290
|
+
}
|
|
291
|
+
scheduleReconnect();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
connection.addEventListener("message", async (event) => {
|
|
295
|
+
if (socket !== connection) return;
|
|
296
|
+
const message = JSON.parse(String(event.data));
|
|
297
|
+
if (message.type === "paired") {
|
|
298
|
+
clearWatchdog();
|
|
272
299
|
const reusedConnector =
|
|
273
300
|
existingConfig?.connector_id === message.connector.id;
|
|
274
301
|
config = mergePairedConfig(
|
|
@@ -294,8 +321,8 @@ async function connect() {
|
|
|
294
321
|
if (config.onboarding_pending) void submitWorkspaceSnapshot();
|
|
295
322
|
return;
|
|
296
323
|
}
|
|
297
|
-
if (message.type === "authenticated") {
|
|
298
|
-
|
|
324
|
+
if (message.type === "authenticated") {
|
|
325
|
+
clearWatchdog();
|
|
299
326
|
console.log("Connected and waiting for EngineerOS runs.");
|
|
300
327
|
reconnectDelay = 1_000;
|
|
301
328
|
startPings();
|
|
@@ -345,10 +372,10 @@ async function connect() {
|
|
|
345
372
|
await stopProcess(active.child);
|
|
346
373
|
return;
|
|
347
374
|
}
|
|
348
|
-
if (message.type === "connector.revoked") {
|
|
349
|
-
stopped = true;
|
|
350
|
-
console.error("This connector was revoked in EngineerOS.");
|
|
351
|
-
|
|
375
|
+
if (message.type === "connector.revoked") {
|
|
376
|
+
stopped = true;
|
|
377
|
+
console.error("This connector was revoked in EngineerOS.");
|
|
378
|
+
connection.close();
|
|
352
379
|
return;
|
|
353
380
|
}
|
|
354
381
|
if (message.type === "run.error") {
|
|
@@ -364,9 +391,10 @@ async function connect() {
|
|
|
364
391
|
console.error(`EngineerOS: ${message.message}`);
|
|
365
392
|
}
|
|
366
393
|
});
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
394
|
+
connection.addEventListener("close", (event) => {
|
|
395
|
+
clearWatchdog();
|
|
396
|
+
if (socket !== connection) return;
|
|
397
|
+
clearInterval(pingTimer);
|
|
370
398
|
for (const promptState of activePrompts.values())
|
|
371
399
|
void cancelPrompt(promptState);
|
|
372
400
|
for (const assessmentState of activeAssessments.values()) {
|
|
@@ -382,8 +410,9 @@ async function connect() {
|
|
|
382
410
|
if (stopped) return;
|
|
383
411
|
scheduleReconnect();
|
|
384
412
|
});
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
connection.addEventListener("error", (event) => {
|
|
414
|
+
if (socket !== connection) return;
|
|
415
|
+
lastConnectionError = describeWebSocketError(event);
|
|
387
416
|
console.error(
|
|
388
417
|
`WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
|
|
389
418
|
);
|
|
@@ -662,9 +691,10 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
662
691
|
let agentCompleted = false;
|
|
663
692
|
let statusTicks = 0;
|
|
664
693
|
let progress = 5;
|
|
665
|
-
let accepted = false;
|
|
666
|
-
let failureReported = false;
|
|
667
|
-
let inactivityFailure = null;
|
|
694
|
+
let accepted = false;
|
|
695
|
+
let failureReported = false;
|
|
696
|
+
let inactivityFailure = null;
|
|
697
|
+
let checkpointSessionId = null;
|
|
668
698
|
let lastReportedMilestone = null;
|
|
669
699
|
const creditedMilestones = new Set();
|
|
670
700
|
const isActiveAssessment = () =>
|
|
@@ -724,7 +754,21 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
724
754
|
);
|
|
725
755
|
}
|
|
726
756
|
}, 15_000);
|
|
727
|
-
const assessmentAgentCallbacks = {
|
|
757
|
+
const assessmentAgentCallbacks = {
|
|
758
|
+
onSession: async (sessionId) => {
|
|
759
|
+
if (!isActiveAssessment()) return;
|
|
760
|
+
checkpointSessionId = String(sessionId);
|
|
761
|
+
await persistAssessmentStageSession(
|
|
762
|
+
config.workspace,
|
|
763
|
+
assessmentId,
|
|
764
|
+
stage,
|
|
765
|
+
sessionId,
|
|
766
|
+
{
|
|
767
|
+
sourceRevision: assignment.source_revision,
|
|
768
|
+
targetHeadRevision: assignment.target_head_revision,
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
},
|
|
728
772
|
onController: (controller) => {
|
|
729
773
|
if (isActiveAssessment()) assessmentState.controller = controller;
|
|
730
774
|
},
|
|
@@ -753,15 +797,16 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
753
797
|
config,
|
|
754
798
|
assessmentAgentCallbacks,
|
|
755
799
|
{
|
|
756
|
-
draftPath: path
|
|
757
|
-
.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
.
|
|
800
|
+
draftPath: path
|
|
801
|
+
.relative(
|
|
802
|
+
config.workspace,
|
|
803
|
+
path.join(
|
|
804
|
+
assessmentRunDirectory(config.workspace, assessmentId),
|
|
805
|
+
candidate.report_file,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
.split(path.sep)
|
|
809
|
+
.join("/"),
|
|
765
810
|
},
|
|
766
811
|
);
|
|
767
812
|
try {
|
|
@@ -773,20 +818,37 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
773
818
|
config.workspace,
|
|
774
819
|
assignment,
|
|
775
820
|
);
|
|
776
|
-
let result = await loadAssessmentStageResult(
|
|
777
|
-
config.workspace,
|
|
778
|
-
assessmentId,
|
|
779
|
-
stage,
|
|
780
|
-
|
|
781
|
-
|
|
821
|
+
let result = await loadAssessmentStageResult(
|
|
822
|
+
config.workspace,
|
|
823
|
+
assessmentId,
|
|
824
|
+
stage,
|
|
825
|
+
{
|
|
826
|
+
expectedHeadRevision: assignment.target_head_revision,
|
|
827
|
+
expectedSourceRevision: assignment.source_revision,
|
|
828
|
+
},
|
|
829
|
+
);
|
|
830
|
+
if (result) {
|
|
782
831
|
attachStoredCorrection(result, preparedAssignment);
|
|
783
832
|
reportProgress("Recovered completed stage report from connector storage");
|
|
784
|
-
} else {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
833
|
+
} else {
|
|
834
|
+
checkpointSessionId = await loadAssessmentStageSession(
|
|
835
|
+
config.workspace,
|
|
836
|
+
assessmentId,
|
|
837
|
+
stage,
|
|
838
|
+
{
|
|
839
|
+
expectedHeadRevision: assignment.target_head_revision,
|
|
840
|
+
expectedSourceRevision: assignment.source_revision,
|
|
841
|
+
},
|
|
842
|
+
);
|
|
843
|
+
if (checkpointSessionId) {
|
|
844
|
+
reportProgress("Restoring interrupted agent session");
|
|
845
|
+
}
|
|
846
|
+
result = await executeWorkspaceAssessment(
|
|
847
|
+
preparedAssignment,
|
|
848
|
+
config,
|
|
849
|
+
assessmentAgentCallbacks,
|
|
850
|
+
{ previousSessionId: checkpointSessionId },
|
|
851
|
+
);
|
|
790
852
|
await persistAssessmentStageResult(
|
|
791
853
|
config.workspace,
|
|
792
854
|
assessmentId,
|
|
@@ -837,11 +899,12 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
837
899
|
submit: deliverResult,
|
|
838
900
|
payloadFor: (candidate) =>
|
|
839
901
|
stage === "synthesis"
|
|
840
|
-
? assessmentCompletionBundle(
|
|
841
|
-
config.workspace,
|
|
842
|
-
assessmentId,
|
|
843
|
-
candidate,
|
|
844
|
-
|
|
902
|
+
? assessmentCompletionBundle(
|
|
903
|
+
config.workspace,
|
|
904
|
+
assessmentId,
|
|
905
|
+
candidate,
|
|
906
|
+
{ expectedSourceRevision: assignment.source_revision },
|
|
907
|
+
)
|
|
845
908
|
: assessmentCheckpointPayload(candidate),
|
|
846
909
|
rejectionFor: (response) =>
|
|
847
910
|
describeRejectedResponse(response, "the assessment"),
|
|
@@ -883,28 +946,56 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
883
946
|
await describeRejectedResponse(response, "the assessment"),
|
|
884
947
|
);
|
|
885
948
|
}
|
|
886
|
-
accepted = true;
|
|
887
|
-
if (stage === "synthesis") {
|
|
888
|
-
await
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
949
|
+
accepted = true;
|
|
950
|
+
if (stage === "synthesis") {
|
|
951
|
+
const archivePath = await archiveCompletedAssessment(
|
|
952
|
+
config.workspace,
|
|
953
|
+
assessmentId,
|
|
954
|
+
assignment,
|
|
955
|
+
result,
|
|
956
|
+
);
|
|
957
|
+
console.log(
|
|
958
|
+
`Workspace assessment ${assessmentId} was accepted by EngineerOS and saved to ${path.relative(config.workspace, archivePath)}.`,
|
|
959
|
+
);
|
|
892
960
|
} else {
|
|
893
961
|
console.log(
|
|
894
962
|
`Workspace assessment stage ${stage} was checkpointed by EngineerOS.`,
|
|
895
963
|
);
|
|
896
964
|
}
|
|
897
|
-
} catch (error) {
|
|
898
|
-
const message = inactivityFailure || protocolFailureMessage(error);
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const message = inactivityFailure || protocolFailureMessage(error);
|
|
967
|
+
const resumeAttempts = Number(
|
|
968
|
+
assignment.connector_session_resume_attempts || 0,
|
|
969
|
+
);
|
|
970
|
+
const canResumeSession =
|
|
971
|
+
!inactivityFailure &&
|
|
972
|
+
checkpointSessionId &&
|
|
973
|
+
isRecoverableAssessmentAgentFailure(error) &&
|
|
974
|
+
resumeAttempts < 2;
|
|
975
|
+
if (isActiveAssessment() && canResumeSession) {
|
|
976
|
+
assessmentState.phase = "waiting";
|
|
977
|
+
assessmentState.lastMessage =
|
|
978
|
+
"Agent session interrupted; resuming checkpoint";
|
|
979
|
+
assessmentState.resumeAssignment = {
|
|
980
|
+
...assignment,
|
|
981
|
+
connector_session_resume_attempts: resumeAttempts + 1,
|
|
982
|
+
};
|
|
983
|
+
const delayMs = 1_000 * 2 ** resumeAttempts;
|
|
984
|
+
console.warn(
|
|
985
|
+
`[assessment:${stage}] Agent session interrupted: ${message} ` +
|
|
986
|
+
`Resuming session ${checkpointSessionId} in ${Math.round(delayMs / 1_000)}s ` +
|
|
987
|
+
`(${resumeAttempts + 1}/2).`,
|
|
988
|
+
);
|
|
989
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
990
|
+
} else if (isActiveAssessment()) {
|
|
991
|
+
failureReported = sendAssessmentMessage({
|
|
992
|
+
type: "workspace.assessment.failed",
|
|
993
|
+
assessment_id: assessmentId,
|
|
994
|
+
stage,
|
|
995
|
+
message,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
if (!canResumeSession) console.error(message);
|
|
908
999
|
} finally {
|
|
909
1000
|
clearInterval(heartbeat);
|
|
910
1001
|
if (isActiveAssessment()) {
|
|
@@ -913,10 +1004,12 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
913
1004
|
assessments,
|
|
914
1005
|
assessmentState,
|
|
915
1006
|
{ accepted, failureReported },
|
|
916
|
-
);
|
|
917
|
-
if (replayQueued) {
|
|
918
|
-
console.log(
|
|
919
|
-
|
|
1007
|
+
);
|
|
1008
|
+
if (replayQueued) {
|
|
1009
|
+
console.log(
|
|
1010
|
+
`Resuming interrupted agent session for assessment stage ${stage} from its checkpoint.`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
920
1013
|
pump();
|
|
921
1014
|
}
|
|
922
1015
|
}
|
package/package.json
CHANGED
package/src/acp-client.mjs
CHANGED
|
@@ -201,19 +201,20 @@ class AcpRuntime {
|
|
|
201
201
|
this.clearIdleTimer();
|
|
202
202
|
const initialized = await this.ready;
|
|
203
203
|
let session = this.sessions.get(sessionKey);
|
|
204
|
-
if (!session) {
|
|
205
|
-
session = await this.openSession(
|
|
204
|
+
if (!session) {
|
|
205
|
+
session = await this.openSession(
|
|
206
206
|
sessionKey,
|
|
207
207
|
previousSessionId,
|
|
208
208
|
sandbox,
|
|
209
209
|
profile,
|
|
210
210
|
);
|
|
211
|
-
callbacks.onEvent?.({
|
|
212
|
-
type: "agent.connected",
|
|
213
|
-
message: `Agent connected with protocol ${initialized.protocolVersion}`,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
211
|
+
callbacks.onEvent?.({
|
|
212
|
+
type: "agent.connected",
|
|
213
|
+
message: `Agent connected with protocol ${initialized.protocolVersion}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
await callbacks.onSession?.(session.sessionId);
|
|
217
|
+
if (this.turns.has(session.sessionId)) {
|
|
217
218
|
throw new Error(
|
|
218
219
|
"The agent is already answering another prompt in this session.",
|
|
219
220
|
);
|
package/src/assessment-spool.mjs
CHANGED
|
@@ -12,14 +12,18 @@ import path from "node:path";
|
|
|
12
12
|
|
|
13
13
|
const LOCAL_REPORTS_MARKER = "<!-- ENGINEEROS_LOCAL_STAGE_REPORTS -->";
|
|
14
14
|
|
|
15
|
-
export function assessmentRunDirectory(workspace,
|
|
16
|
-
return path.join(
|
|
17
|
-
path.resolve(workspace),
|
|
18
|
-
".engineeros",
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
}
|
|
15
|
+
export function assessmentRunDirectory(workspace, _assessmentId) {
|
|
16
|
+
return path.join(
|
|
17
|
+
path.resolve(workspace),
|
|
18
|
+
".engineeros",
|
|
19
|
+
"assessment",
|
|
20
|
+
".work",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assessmentArchiveDirectory(workspace) {
|
|
25
|
+
return path.join(path.resolve(workspace), ".engineeros", "assessment");
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
export async function persistAssessmentStageResult(
|
|
25
29
|
workspace,
|
|
@@ -33,12 +37,14 @@ export async function persistAssessmentStageResult(
|
|
|
33
37
|
const basename = assessmentStageBasename(result.stage);
|
|
34
38
|
const reportName = `${basename}.md`;
|
|
35
39
|
const metadataName = `${basename}.result.json`;
|
|
36
|
-
const report = String(result.report_markdown).trim();
|
|
37
|
-
const metadata = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const report = String(result.report_markdown).trim();
|
|
41
|
+
const metadata = {
|
|
42
|
+
assessment_id: assessmentId,
|
|
43
|
+
stage: result.stage,
|
|
44
|
+
report_file: reportName,
|
|
45
|
+
report_sha256: sha256(report),
|
|
46
|
+
source_revision: assignment.source_revision ?? null,
|
|
47
|
+
observed_head_revision: result.observed_head_revision ?? null,
|
|
42
48
|
changed_files: result.changed_files ?? [],
|
|
43
49
|
change_impact_markdown: result.change_impact_markdown ?? null,
|
|
44
50
|
model: result.model ?? null,
|
|
@@ -53,23 +59,39 @@ export async function persistAssessmentStageResult(
|
|
|
53
59
|
return { ...metadata, report_markdown: report };
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
export async function loadAssessmentStageResult(
|
|
57
|
-
workspace,
|
|
58
|
-
assessmentId,
|
|
59
|
-
stage,
|
|
60
|
-
|
|
62
|
+
export async function loadAssessmentStageResult(
|
|
63
|
+
workspace,
|
|
64
|
+
assessmentId,
|
|
65
|
+
stage,
|
|
66
|
+
{ expectedHeadRevision, expectedSourceRevision } = {},
|
|
67
|
+
) {
|
|
61
68
|
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
62
69
|
const metadataPath = path.join(
|
|
63
70
|
directory,
|
|
64
71
|
`${assessmentStageBasename(stage)}.result.json`,
|
|
65
72
|
);
|
|
66
73
|
try {
|
|
67
|
-
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
|
|
68
|
-
if (metadata.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
|
|
75
|
+
if (metadata.assessment_id !== assessmentId) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (metadata.stage !== stage) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Assessment spool metadata does not match stage '${stage}'.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
expectedHeadRevision &&
|
|
85
|
+
metadata.observed_head_revision !== expectedHeadRevision
|
|
86
|
+
) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
expectedSourceRevision &&
|
|
91
|
+
metadata.source_revision !== expectedSourceRevision
|
|
92
|
+
) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
73
95
|
const expectedReportFile = `${assessmentStageBasename(stage)}.md`;
|
|
74
96
|
if (metadata.report_file !== expectedReportFile) {
|
|
75
97
|
throw new Error(
|
|
@@ -89,7 +111,78 @@ export async function loadAssessmentStageResult(
|
|
|
89
111
|
if (error?.code === "ENOENT") return null;
|
|
90
112
|
throw error;
|
|
91
113
|
}
|
|
92
|
-
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function persistAssessmentStageSession(
|
|
117
|
+
workspace,
|
|
118
|
+
assessmentId,
|
|
119
|
+
stage,
|
|
120
|
+
sessionId,
|
|
121
|
+
{ sourceRevision, targetHeadRevision } = {},
|
|
122
|
+
) {
|
|
123
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
124
|
+
if (!normalizedSessionId) {
|
|
125
|
+
throw new Error(`Assessment stage '${stage}' returned an empty session id.`);
|
|
126
|
+
}
|
|
127
|
+
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
128
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
129
|
+
const checkpoint = {
|
|
130
|
+
assessment_id: assessmentId,
|
|
131
|
+
stage,
|
|
132
|
+
agent_session_id: normalizedSessionId,
|
|
133
|
+
source_revision: String(sourceRevision || "").trim() || null,
|
|
134
|
+
target_head_revision: String(targetHeadRevision || "").trim() || null,
|
|
135
|
+
};
|
|
136
|
+
await atomicWrite(
|
|
137
|
+
assessmentStageSessionPath(directory, stage),
|
|
138
|
+
`${JSON.stringify(checkpoint, null, 2)}\n`,
|
|
139
|
+
);
|
|
140
|
+
return checkpoint;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function loadAssessmentStageSession(
|
|
144
|
+
workspace,
|
|
145
|
+
assessmentId,
|
|
146
|
+
stage,
|
|
147
|
+
{ expectedHeadRevision, expectedSourceRevision } = {},
|
|
148
|
+
) {
|
|
149
|
+
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
150
|
+
try {
|
|
151
|
+
const checkpoint = JSON.parse(
|
|
152
|
+
await readFile(assessmentStageSessionPath(directory, stage), "utf8"),
|
|
153
|
+
);
|
|
154
|
+
if (checkpoint.assessment_id !== assessmentId) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (checkpoint.stage !== stage) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Assessment session checkpoint does not match stage '${stage}'.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
expectedHeadRevision &&
|
|
164
|
+
checkpoint.target_head_revision !== expectedHeadRevision
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
expectedSourceRevision &&
|
|
170
|
+
checkpoint.source_revision !== expectedSourceRevision
|
|
171
|
+
) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const sessionId = String(checkpoint.agent_session_id || "").trim();
|
|
175
|
+
if (!sessionId) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Assessment session checkpoint for '${stage}' has no session id.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return sessionId;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error?.code === "ENOENT") return null;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
93
186
|
|
|
94
187
|
export async function removeAssessmentStageResult(
|
|
95
188
|
workspace,
|
|
@@ -112,7 +205,11 @@ export async function removeAssessmentStageResult(
|
|
|
112
205
|
);
|
|
113
206
|
}
|
|
114
207
|
|
|
115
|
-
export async function assessmentStageResults(
|
|
208
|
+
export async function assessmentStageResults(
|
|
209
|
+
workspace,
|
|
210
|
+
assessmentId,
|
|
211
|
+
{ expectedHeadRevision, expectedSourceRevision } = {},
|
|
212
|
+
) {
|
|
116
213
|
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
117
214
|
let entries;
|
|
118
215
|
try {
|
|
@@ -128,11 +225,12 @@ export async function assessmentStageResults(workspace, assessmentId) {
|
|
|
128
225
|
const metadata = JSON.parse(
|
|
129
226
|
await readFile(path.join(directory, entry), "utf8"),
|
|
130
227
|
);
|
|
131
|
-
return loadAssessmentStageResult(
|
|
132
|
-
workspace,
|
|
133
|
-
assessmentId,
|
|
134
|
-
metadata.stage,
|
|
135
|
-
|
|
228
|
+
return loadAssessmentStageResult(
|
|
229
|
+
workspace,
|
|
230
|
+
assessmentId,
|
|
231
|
+
metadata.stage,
|
|
232
|
+
{ expectedHeadRevision, expectedSourceRevision },
|
|
233
|
+
);
|
|
136
234
|
}),
|
|
137
235
|
);
|
|
138
236
|
return results
|
|
@@ -144,7 +242,10 @@ export async function prepareLocalAssessmentAssignment(workspace, assignment) {
|
|
|
144
242
|
const prompt = String(assignment.prompt_markdown || "");
|
|
145
243
|
if (!prompt.includes(LOCAL_REPORTS_MARKER)) return assignment;
|
|
146
244
|
const reports = (
|
|
147
|
-
await assessmentStageResults(workspace, assignment.assessment_id
|
|
245
|
+
await assessmentStageResults(workspace, assignment.assessment_id, {
|
|
246
|
+
expectedHeadRevision: assignment.target_head_revision,
|
|
247
|
+
expectedSourceRevision: assignment.source_revision,
|
|
248
|
+
})
|
|
148
249
|
).filter((result) => result.stage !== "synthesis");
|
|
149
250
|
if (!reports.length) {
|
|
150
251
|
throw new Error(
|
|
@@ -176,19 +277,71 @@ export async function prepareLocalAssessmentAssignment(workspace, assignment) {
|
|
|
176
277
|
};
|
|
177
278
|
}
|
|
178
279
|
|
|
179
|
-
export async function assessmentCompletionBundle(
|
|
180
|
-
workspace,
|
|
181
|
-
assessmentId,
|
|
182
|
-
synthesisResult,
|
|
183
|
-
|
|
280
|
+
export async function assessmentCompletionBundle(
|
|
281
|
+
workspace,
|
|
282
|
+
assessmentId,
|
|
283
|
+
synthesisResult,
|
|
284
|
+
{ expectedSourceRevision } = {},
|
|
285
|
+
) {
|
|
184
286
|
const reports = (
|
|
185
|
-
await assessmentStageResults(workspace, assessmentId
|
|
287
|
+
await assessmentStageResults(workspace, assessmentId, {
|
|
288
|
+
expectedHeadRevision: synthesisResult.observed_head_revision,
|
|
289
|
+
expectedSourceRevision,
|
|
290
|
+
})
|
|
186
291
|
).filter((result) => result.stage !== "synthesis");
|
|
187
292
|
return {
|
|
188
293
|
...assessmentResultPayload(synthesisResult),
|
|
189
294
|
reports: reports.map(assessmentResultPayload),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function archiveCompletedAssessment(
|
|
299
|
+
workspace,
|
|
300
|
+
assessmentId,
|
|
301
|
+
assignment,
|
|
302
|
+
synthesisResult,
|
|
303
|
+
) {
|
|
304
|
+
const bundle = await assessmentCompletionBundle(
|
|
305
|
+
workspace,
|
|
306
|
+
assessmentId,
|
|
307
|
+
synthesisResult,
|
|
308
|
+
{ expectedSourceRevision: assignment.source_revision },
|
|
309
|
+
);
|
|
310
|
+
const archiveDirectory = assessmentArchiveDirectory(workspace);
|
|
311
|
+
const isUpdate = assignment.is_assessment_update === true;
|
|
312
|
+
const target = isUpdate
|
|
313
|
+
? path.join(
|
|
314
|
+
archiveDirectory,
|
|
315
|
+
"changes",
|
|
316
|
+
`${assessmentChangeName(assignment, synthesisResult, assessmentId)}.md`,
|
|
317
|
+
)
|
|
318
|
+
: path.join(archiveDirectory, "assessment.md");
|
|
319
|
+
const heading = isUpdate
|
|
320
|
+
? "# Workspace Assessment Change Log"
|
|
321
|
+
: "# Workspace Assessment";
|
|
322
|
+
const stageReports = [...bundle.reports, assessmentResultPayload(synthesisResult)]
|
|
323
|
+
.map(
|
|
324
|
+
(report) =>
|
|
325
|
+
`## ${assessmentStageTitle(report.stage, report.report_markdown)}\n\n${nestedStageReport(report.report_markdown)}`,
|
|
326
|
+
)
|
|
327
|
+
.join("\n\n");
|
|
328
|
+
const content = [
|
|
329
|
+
heading,
|
|
330
|
+
"",
|
|
331
|
+
`- Inventory revision: \`${assignment.source_revision || "not available"}\``,
|
|
332
|
+
`- Git commit: \`${synthesisResult.observed_head_revision || "not available"}\``,
|
|
333
|
+
"",
|
|
334
|
+
stageReports,
|
|
335
|
+
"",
|
|
336
|
+
].join("\n");
|
|
337
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
338
|
+
await atomicWrite(target, content);
|
|
339
|
+
await rm(assessmentRunDirectory(workspace, assessmentId), {
|
|
340
|
+
recursive: true,
|
|
341
|
+
force: true,
|
|
342
|
+
});
|
|
343
|
+
return target;
|
|
344
|
+
}
|
|
192
345
|
|
|
193
346
|
export function assessmentCheckpointPayload(result) {
|
|
194
347
|
return {
|
|
@@ -226,14 +379,59 @@ function validateLocalAssessmentResult(assignment, result) {
|
|
|
226
379
|
}
|
|
227
380
|
}
|
|
228
381
|
|
|
229
|
-
function assessmentStageBasename(stage) {
|
|
382
|
+
function assessmentStageBasename(stage) {
|
|
230
383
|
const slug =
|
|
231
384
|
String(stage)
|
|
232
385
|
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
|
233
386
|
.replace(/^-+|-+$/g, "")
|
|
234
387
|
.slice(0, 64) || "stage";
|
|
235
388
|
return `${slug}-${sha256(String(stage)).slice(0, 12)}`;
|
|
236
|
-
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function assessmentStageTitle(stage, markdown) {
|
|
392
|
+
const stageKey = String(stage || "");
|
|
393
|
+
let namedStage = null;
|
|
394
|
+
let label = "";
|
|
395
|
+
if (stageKey.startsWith("capability:")) {
|
|
396
|
+
label = "Capability";
|
|
397
|
+
namedStage = String(markdown || "").match(/^### Capability:\s*(.+?)\s*$/m);
|
|
398
|
+
} else if (stageKey.startsWith("compliance:")) {
|
|
399
|
+
label = "Compliance";
|
|
400
|
+
namedStage = String(markdown || "").match(
|
|
401
|
+
/^### Compliance Framework:\s*(.+?)\s*$/m,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (namedStage) {
|
|
405
|
+
return `${label} — ${namedStage[1].trim()}`;
|
|
406
|
+
}
|
|
407
|
+
return String(stage || "assessment")
|
|
408
|
+
.replace(/[:_-]+/g, " ")
|
|
409
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function nestedStageReport(markdown) {
|
|
413
|
+
return String(markdown || "")
|
|
414
|
+
.trim()
|
|
415
|
+
.replace(/^# Assessment Stage:[^\n]*\n+/i, "")
|
|
416
|
+
.replace(/^\s*-\s+State:\s*.*\n?/gm, "")
|
|
417
|
+
.replace(/^(#{2,5}) /gm, "#$1 ");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function assessmentChangeName(assignment, synthesisResult, assessmentId) {
|
|
421
|
+
const headRevision = String(synthesisResult.observed_head_revision || "")
|
|
422
|
+
.trim()
|
|
423
|
+
.toLowerCase();
|
|
424
|
+
if (/^[a-f0-9]{7,64}$/.test(headRevision)) {
|
|
425
|
+
return headRevision.slice(0, 16);
|
|
426
|
+
}
|
|
427
|
+
return sha256(
|
|
428
|
+
String(assignment.source_revision || assessmentId),
|
|
429
|
+
).slice(0, 16);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function assessmentStageSessionPath(directory, stage) {
|
|
433
|
+
return path.join(directory, `${assessmentStageBasename(stage)}.session.json`);
|
|
434
|
+
}
|
|
237
435
|
|
|
238
436
|
function sha256(value) {
|
|
239
437
|
return createHash("sha256").update(value).digest("hex");
|
package/src/codex-app-server.mjs
CHANGED
|
@@ -217,8 +217,9 @@ export function launchCodexAppServer({
|
|
|
217
217
|
model: profile.model || null,
|
|
218
218
|
ephemeral: false,
|
|
219
219
|
});
|
|
220
|
-
threadId = threadResult.thread.id;
|
|
221
|
-
callbacks.
|
|
220
|
+
threadId = threadResult.thread.id;
|
|
221
|
+
await callbacks.onSession?.(threadId);
|
|
222
|
+
callbacks.onEvent?.({ type: "thread.started", thread_id: threadId });
|
|
222
223
|
const turnResult = await request("turn/start", {
|
|
223
224
|
threadId,
|
|
224
225
|
input: [{ type: "text", text: prompt }],
|
package/src/connection.mjs
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
const PROTOCOL_FAILURE_MESSAGE_MAX_LENGTH = 2_000;
|
|
2
2
|
const TRUNCATED_FAILURE_SUFFIX = "\n… [truncated by EngineerOS connector]";
|
|
3
3
|
|
|
4
|
-
export function describeWebSocketError(event) {
|
|
4
|
+
export function describeWebSocketError(event) {
|
|
5
5
|
return (
|
|
6
6
|
event?.error?.message ||
|
|
7
7
|
event?.message ||
|
|
8
8
|
"The WebSocket connection failed without providing an error detail."
|
|
9
9
|
);
|
|
10
|
-
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sendConnectionMessage(activeSocket, connection, message) {
|
|
13
|
+
if (activeSocket !== connection || connection.readyState !== 1) return false;
|
|
14
|
+
connection.send(JSON.stringify(message));
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
export function protocolFailureMessage(error) {
|
|
13
19
|
const raw = error instanceof Error ? error.message : String(error ?? "");
|
package/src/runner.mjs
CHANGED
|
@@ -361,11 +361,12 @@ export function parseVerificationReport(
|
|
|
361
361
|
});
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
export async function executeWorkspaceAssessment(
|
|
365
|
-
assignment,
|
|
366
|
-
config,
|
|
367
|
-
callbacks,
|
|
368
|
-
|
|
364
|
+
export async function executeWorkspaceAssessment(
|
|
365
|
+
assignment,
|
|
366
|
+
config,
|
|
367
|
+
callbacks,
|
|
368
|
+
{ previousSessionId } = {},
|
|
369
|
+
) {
|
|
369
370
|
const execution = workspaceAssessmentExecution(assignment);
|
|
370
371
|
const assessmentAcpContext =
|
|
371
372
|
config.agent_protocol === "acp"
|
|
@@ -415,13 +416,16 @@ export async function executeWorkspaceAssessment(
|
|
|
415
416
|
assignment.target_head_revision,
|
|
416
417
|
)
|
|
417
418
|
: { changedFiles: [], markdown: null };
|
|
418
|
-
const prompt = changeImpact.markdown
|
|
419
|
-
? execution.prompt.replace(
|
|
420
|
-
"<!-- ENGINEEROS_CHANGE_IMPACT -->",
|
|
421
|
-
changeImpact.markdown,
|
|
422
|
-
)
|
|
423
|
-
: execution.prompt;
|
|
424
|
-
const controller = launchAssessmentAgent(
|
|
419
|
+
const prompt = changeImpact.markdown
|
|
420
|
+
? execution.prompt.replace(
|
|
421
|
+
"<!-- ENGINEEROS_CHANGE_IMPACT -->",
|
|
422
|
+
changeImpact.markdown,
|
|
423
|
+
)
|
|
424
|
+
: execution.prompt;
|
|
425
|
+
const controller = launchAssessmentAgent(
|
|
426
|
+
previousSessionId ? assessmentContinuationPrompt(prompt) : prompt,
|
|
427
|
+
previousSessionId,
|
|
428
|
+
);
|
|
425
429
|
let completed = await controller.completed;
|
|
426
430
|
const recovered = await recoverAssessmentStageOutput({
|
|
427
431
|
completed,
|
|
@@ -1099,6 +1103,38 @@ export function takeWorkspaceAssessmentWave(queue, activeCount, limit) {
|
|
|
1099
1103
|
return wave;
|
|
1100
1104
|
}
|
|
1101
1105
|
|
|
1106
|
+
export function assessmentContinuationPrompt(originalPrompt) {
|
|
1107
|
+
return [
|
|
1108
|
+
"# Continue Interrupted EngineerOS Assessment",
|
|
1109
|
+
"",
|
|
1110
|
+
"Continue the existing assessment task in this agent session from its last completed step.",
|
|
1111
|
+
"Do not restart repository inspection or repeat work already present in the session context.",
|
|
1112
|
+
"Finish and return the complete structured Markdown report required by the original stage contract.",
|
|
1113
|
+
"If the provider could not restore prior context, use the original contract below to complete only the missing work.",
|
|
1114
|
+
"",
|
|
1115
|
+
"## Original Stage Contract",
|
|
1116
|
+
"",
|
|
1117
|
+
String(originalPrompt || "").trim(),
|
|
1118
|
+
].join("\n");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function isRecoverableAssessmentAgentFailure(error) {
|
|
1122
|
+
const message = String(error?.message || error || "").toLowerCase();
|
|
1123
|
+
return [
|
|
1124
|
+
"acp connection closed",
|
|
1125
|
+
"acp agent ended the task with an error",
|
|
1126
|
+
"app-server connection closed",
|
|
1127
|
+
"app server connection closed",
|
|
1128
|
+
"connection reset",
|
|
1129
|
+
"econnreset",
|
|
1130
|
+
"broken pipe",
|
|
1131
|
+
"service unavailable",
|
|
1132
|
+
"too many requests",
|
|
1133
|
+
"rate limit",
|
|
1134
|
+
"overloaded",
|
|
1135
|
+
].some((fragment) => message.includes(fragment));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1102
1138
|
export function workspaceAssessmentWorkerLimit(value) {
|
|
1103
1139
|
const parsed = Number(value ?? DEFAULT_ASSESSMENT_WORKERS);
|
|
1104
1140
|
if (
|
|
@@ -1674,8 +1710,10 @@ function launchCodexProcess(
|
|
|
1674
1710
|
child.stdin.end(prompt);
|
|
1675
1711
|
let output = "";
|
|
1676
1712
|
let buffer = "";
|
|
1677
|
-
let finalMessage = "";
|
|
1678
|
-
let sessionId = previousSessionId || "";
|
|
1713
|
+
let finalMessage = "";
|
|
1714
|
+
let sessionId = previousSessionId || "";
|
|
1715
|
+
let sessionCheckpoint = Promise.resolve();
|
|
1716
|
+
let sessionCheckpointError;
|
|
1679
1717
|
child.stdout.setEncoding("utf8");
|
|
1680
1718
|
child.stdout.on("data", (chunk) => {
|
|
1681
1719
|
output += chunk;
|
|
@@ -1686,12 +1724,17 @@ function launchCodexProcess(
|
|
|
1686
1724
|
if (!line.trim()) continue;
|
|
1687
1725
|
try {
|
|
1688
1726
|
const event = JSON.parse(line);
|
|
1689
|
-
if (
|
|
1690
|
-
event.type === "thread.started" &&
|
|
1691
|
-
typeof event.thread_id === "string"
|
|
1692
|
-
) {
|
|
1693
|
-
sessionId = event.thread_id;
|
|
1694
|
-
|
|
1727
|
+
if (
|
|
1728
|
+
event.type === "thread.started" &&
|
|
1729
|
+
typeof event.thread_id === "string"
|
|
1730
|
+
) {
|
|
1731
|
+
sessionId = event.thread_id;
|
|
1732
|
+
sessionCheckpoint = Promise.resolve()
|
|
1733
|
+
.then(() => callbacks.onSession?.(sessionId))
|
|
1734
|
+
.catch((error) => {
|
|
1735
|
+
sessionCheckpointError = error;
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1695
1738
|
if (
|
|
1696
1739
|
event.type === "item.completed" &&
|
|
1697
1740
|
event.item?.type === "agent_message" &&
|
|
@@ -1718,17 +1761,20 @@ function launchCodexProcess(
|
|
|
1718
1761
|
});
|
|
1719
1762
|
const completed = new Promise((resolve, reject) => {
|
|
1720
1763
|
child.once("error", reject);
|
|
1721
|
-
child.once("close", (code) => {
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1764
|
+
child.once("close", (code) => {
|
|
1765
|
+
void sessionCheckpoint.then(() => {
|
|
1766
|
+
if (sessionCheckpointError) reject(sessionCheckpointError);
|
|
1767
|
+
else if (code === 0 && sessionId)
|
|
1768
|
+
resolve({ output: output.slice(-20_000), finalMessage, sessionId });
|
|
1769
|
+
else if (code === 0)
|
|
1770
|
+
reject(
|
|
1771
|
+
new Error(
|
|
1772
|
+
"Agent completed without announcing a resumable session id.",
|
|
1773
|
+
),
|
|
1774
|
+
);
|
|
1775
|
+
else reject(new Error(codexFailureMessage(output, code)));
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1732
1778
|
});
|
|
1733
1779
|
return { child, completed };
|
|
1734
1780
|
}
|