Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1847,16 +1847,31 @@ class Orchestrator {
},
},
});
const boundary = this._clusterRunBoundaries.get(clusterId);
const complete = messageBus.query({
cluster_id: clusterId,
topic: 'CLUSTER_COMPLETE',
limit: 1,
...(boundary === null || boundary === undefined ? {} : { afterId: boundary }),
})[0];
const failed = this._findCurrentRunClusterFailure(messageBus, clusterId);
if (!complete && !failed) {
messageBus.publish({
cluster_id: clusterId,
topic: 'CLUSTER_FAILED',
sender: 'orchestrator',
content: {
text: `Operation chain failed: ${err.message}`,
data: { reason: 'cluster_operations_failed' },
},
});
}

this._log(`\n${'='.repeat(80)}`);
this._log(`❌ CLUSTER_OPERATIONS FAILED - STOPPING CLUSTER`);
this._log(`${'='.repeat(80)}`);
this._log(`Error: ${err.message}`);
this._log(`${'='.repeat(80)}\n`);

this.stop(clusterId).catch((stopErr) => {
console.error(`Failed to stop cluster after operation failure:`, stopErr.message);
});
});
});
}
Expand Down
17 changes: 14 additions & 3 deletions tests/integration/orchestrator-flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1117,10 +1117,10 @@ function defineClusterOperationsFailureTests() {
* Test for the bug where CLUSTER_OPERATIONS failure didn't stop the cluster.
*
* Root cause (fixed): When CLUSTER_OPERATIONS failed (e.g., agent model > maxModel),
* the .catch() handler published CLUSTER_OPERATIONS_FAILED but never called stop().
* The cluster remained running with no working agents.
* the .catch() handler published a nonterminal CLUSTER_OPERATIONS_FAILED event and
* stopped directly, leaving foreground result writers without a terminal event.
*
* Fix: Added this.stop(clusterId) in the catch handler after publishing the failure message.
* Fix: Publish one CLUSTER_FAILED event and let the terminal subscription stop the cluster.
*/
it('should stop cluster when CLUSTER_OPERATIONS fails due to model validation', async function () {
await runClusterOperationsModelFailureTest();
Expand Down Expand Up @@ -1204,6 +1204,11 @@ async function runClusterOperationsModelFailureTest() {
failedMessages[0].content.text.includes('Operation chain failed'),
'Failure message: should indicate operation failure'
);
assert.strictEqual(
cluster.messageBus.query({ cluster_id: clusterId, topic: 'CLUSTER_FAILED' }).length,
1,
'CLUSTER_FAILED: should contain exactly one terminal event'
);

// Verify the cluster state is stopped (not running)
const finalStatus = orchestrator.getStatus(clusterId);
Expand Down Expand Up @@ -1269,6 +1274,12 @@ async function runClusterOperationsValidationFailureTest() {
// Wait for cluster to stop
await waitForClusterState(orchestrator, clusterId, 'stopped', 10000);

assert.strictEqual(
cluster.messageBus.query({ cluster_id: clusterId, topic: 'CLUSTER_FAILED' }).length,
1,
'CLUSTER_FAILED: should contain exactly one terminal event'
);

// Verify the cluster stopped due to the operation failure
const finalStatus = orchestrator.getStatus(clusterId);
assert.strictEqual(
Expand Down
Loading