@engineeros/connector 0.13.3 → 0.13.4

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 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 Architecture, Capabilities, Quality, or any combination when only part of the workspace needs reassessment. EngineerOS stores every completed assessment stage before starting the next one, resumes the unfinished stage after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. The connector prints the current safe activity and elapsed time every 30 seconds, stops a stage that produces no agent activity for ten minutes instead of waiting indefinitely, and gives an incomplete final report one bounded correction turn instead of repeatedly restarting the stage. Capabilities are assessed one discovered capability at a time so a large repository does not depend on one unbounded agent turn. Assessment cannot modify the workspace.
50
+ From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select Architecture, Capabilities, Quality, or any combination when only part of the workspace needs reassessment. EngineerOS stores every completed assessment stage, resumes unfinished work after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. After the capability catalog is accepted, the connector runs up to three isolated read-only capability workers concurrently; each result, live output, validation correction, and retry remains independently durable. 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 gives an incomplete report one bounded correction turn instead of repeatedly restarting it. Assessment cannot modify the workspace.
51
51
 
52
52
  After onboarding, every project prompt is routed to this connection. Copilot, shaping, planning, architecture, and experience generation use the connected agent subscription and workspace context. Interactive prompts run independently from assessments and Goal scheduling. Prompt runs are read-only; only an explicitly registered Goal Run receives workspace-write access. If the connector is offline, EngineerOS asks the user to reconnect instead of silently switching models.
53
53
 
@@ -24,6 +24,7 @@ import {
24
24
  promptStreamEvent,
25
25
  requeueInterruptedAssessment,
26
26
  stopProcess,
27
+ takeWorkspaceAssessmentWave,
27
28
  workspaceSnapshot,
28
29
  } from "../src/runner.mjs";
29
30
  import { disposeAcpRuntimes } from "../src/acp-client.mjs";
@@ -195,6 +196,8 @@ firstMessage.capabilities = capabilities;
195
196
 
196
197
  let stopped = false;
197
198
  let active = null;
199
+ const activeAssessments = new Map();
200
+ const ASSESSMENT_WORKER_LIMIT = 3;
198
201
  const available = [];
199
202
  const assessments = [];
200
203
  const activePrompts = new Map();
@@ -212,7 +215,10 @@ process.on("SIGINT", async () => {
212
215
  clearConnectionWatchdog?.();
213
216
  clearTimeout(reconnectTimer);
214
217
  clearInterval(pingTimer);
215
- await stopProcess(active?.child);
218
+ await Promise.all([
219
+ stopProcess(active?.child),
220
+ ...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
221
+ ]);
216
222
  await Promise.all([...activePrompts.values()].map(cancelPrompt));
217
223
  await disposeAcpRuntimes();
218
224
  socket?.close();
@@ -278,9 +284,10 @@ async function connect() {
278
284
  return;
279
285
  }
280
286
  if (message.type === "workspace.assessment") {
287
+ const assessmentKey = `${message.assessment_id}:${message.stage}`;
281
288
  const disposition = enqueueWorkspaceAssessment(
282
289
  assessments,
283
- active,
290
+ activeAssessments.get(assessmentKey),
284
291
  message,
285
292
  );
286
293
  if (disposition === "deferred") {
@@ -339,9 +346,9 @@ async function connect() {
339
346
  clearInterval(pingTimer);
340
347
  for (const promptState of activePrompts.values())
341
348
  void cancelPrompt(promptState);
342
- if (active?.kind === "assessment") {
343
- active.transportInterrupted = true;
344
- if (event.code === 4001) void stopProcess(active.child);
349
+ for (const assessmentState of activeAssessments.values()) {
350
+ assessmentState.transportInterrupted = true;
351
+ if (event.code === 4001) void stopProcess(assessmentState.child);
345
352
  }
346
353
  if (connectionRejected) {
347
354
  stopped = true;
@@ -463,21 +470,27 @@ function sendPromptEvent(promptId, event) {
463
470
  }
464
471
 
465
472
  function pump() {
466
- if (active || socket.readyState !== WebSocket.OPEN) return;
467
- const assessment = assessments.shift();
468
- if (assessment) {
473
+ if (socket.readyState !== WebSocket.OPEN || active) return;
474
+ const wave = takeWorkspaceAssessmentWave(
475
+ assessments,
476
+ activeAssessments.size,
477
+ ASSESSMENT_WORKER_LIMIT,
478
+ );
479
+ for (const assessment of wave) {
480
+ const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
469
481
  const assessmentState = {
470
482
  kind: "assessment",
483
+ key: assessmentKey,
471
484
  runId: assessment.assessment_id,
472
485
  stage: assessment.stage,
473
486
  child: null,
474
487
  resumeAssignment: null,
475
488
  transportInterrupted: false,
476
489
  };
477
- active = assessmentState;
490
+ activeAssessments.set(assessmentKey, assessmentState);
478
491
  void executeAssessment(assessment, assessmentState);
479
- return;
480
492
  }
493
+ if (activeAssessments.size > 0 || assessments.length > 0) return;
481
494
  const runId = available.shift();
482
495
  if (!runId) return;
483
496
  active = { kind: "goal", runId, child: null, cancelled: false };
@@ -604,8 +617,10 @@ async function executeAssessment(assignment, assessmentState) {
604
617
  let inactivityFailure = null;
605
618
  let lastReportedMilestone = null;
606
619
  const creditedMilestones = new Set();
620
+ const isActiveAssessment = () =>
621
+ activeAssessments.get(assessmentState.key) === assessmentState;
607
622
  const reportProgress = (message, { milestone = true } = {}) => {
608
- if (active !== assessmentState) return;
623
+ if (!isActiveAssessment()) return;
609
624
  if (milestone && lastReportedMilestone === message) return;
610
625
  if (milestone) {
611
626
  lastReportedMilestone = message;
@@ -635,7 +650,7 @@ async function executeAssessment(assignment, assessmentState) {
635
650
  if (
636
651
  !output ||
637
652
  socket.readyState !== WebSocket.OPEN ||
638
- active !== assessmentState
653
+ !isActiveAssessment()
639
654
  ) {
640
655
  return;
641
656
  }
@@ -693,7 +708,7 @@ async function executeAssessment(assignment, assessmentState) {
693
708
  }
694
709
  let result = await executeWorkspaceAssessment(assignment, config, {
695
710
  onProcess: (child) => {
696
- if (active === assessmentState) {
711
+ if (isActiveAssessment()) {
697
712
  assessmentState.child = child;
698
713
  agentProcessStarted = true;
699
714
  lastAgentActivityAt = Date.now();
@@ -747,7 +762,7 @@ async function executeAssessment(assignment, assessmentState) {
747
762
  const message = inactivityFailure || protocolFailureMessage(error);
748
763
  if (
749
764
  !assessmentState.transportInterrupted &&
750
- active === assessmentState &&
765
+ isActiveAssessment() &&
751
766
  socket.readyState === WebSocket.OPEN
752
767
  ) {
753
768
  socket.send(
@@ -764,8 +779,8 @@ async function executeAssessment(assignment, assessmentState) {
764
779
  } finally {
765
780
  if (outputTimer) clearTimeout(outputTimer);
766
781
  clearInterval(heartbeat);
767
- if (active === assessmentState) {
768
- active = null;
782
+ if (isActiveAssessment()) {
783
+ activeAssessments.delete(assessmentState.key);
769
784
  const replayQueued = requeueInterruptedAssessment(
770
785
  assessments,
771
786
  assessmentState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
package/src/runner.mjs CHANGED
@@ -1000,6 +1000,22 @@ export function enqueueWorkspaceAssessment(
1000
1000
  return "queued";
1001
1001
  }
1002
1002
 
1003
+ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
1004
+ const available = Math.max(0, limit - activeCount);
1005
+ if (!available || !queue.length) return [];
1006
+ if (!String(queue[0]?.stage || "").startsWith("capability:")) {
1007
+ return activeCount === 0 ? queue.splice(0, 1) : [];
1008
+ }
1009
+ const wave = [];
1010
+ while (
1011
+ wave.length < available &&
1012
+ String(queue[0]?.stage || "").startsWith("capability:")
1013
+ ) {
1014
+ wave.push(queue.shift());
1015
+ }
1016
+ return wave;
1017
+ }
1018
+
1003
1019
  export function requeueInterruptedAssessment(
1004
1020
  queue,
1005
1021
  assessmentState,