fix(core): resolve scheduler hang and improve policy violation visibility

This PR addresses three core issues with the Policy Engine and Scheduler:
1. Scheduler Hang: Removed a redundant MessageBus listener in Scheduler.ts that caused race conditions in TTY environments.
2. Policy Visibility: Updated ToolGroupMessage.tsx to always display policy violation errors, regardless of verbosity settings.
3. User Feedback: Added emitFeedback to MessageBus.ts to ensure blocked tool calls are reported to the UI.
This commit is contained in:
mkorwel
2026-03-14 12:14:50 -07:00
parent 9f7691fd88
commit e162f622e4
5 changed files with 15 additions and 33 deletions
@@ -21,6 +21,7 @@ import { isShellTool } from './ToolShared.js';
import {
shouldHideToolCall,
CoreToolCallStatus,
ToolErrorType,
} from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
@@ -59,7 +60,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
if (
isLowErrorVerbosity &&
t.status === CoreToolCallStatus.Error &&
!t.isClientInitiated
!t.isClientInitiated &&
t.errorType !== ToolErrorType.POLICY_VIOLATION
) {
return false;
}
+4
View File
@@ -10,6 +10,7 @@ import {
type ToolResultDisplay,
debugLogger,
CoreToolCallStatus,
type ToolErrorType,
} from '@google/gemini-cli-core';
import {
type HistoryItemToolGroup,
@@ -63,6 +64,7 @@ export function mapToDisplay(
let progressMessage: string | undefined = undefined;
let progress: number | undefined = undefined;
let progressTotal: number | undefined = undefined;
let errorType: ToolErrorType | undefined = undefined;
switch (call.status) {
case CoreToolCallStatus.Success:
@@ -72,6 +74,7 @@ export function mapToDisplay(
case CoreToolCallStatus.Error:
case CoreToolCallStatus.Cancelled:
resultDisplay = call.response.resultDisplay;
errorType = call.response.errorType;
break;
case CoreToolCallStatus.AwaitingApproval:
correlationId = call.correlationId;
@@ -114,6 +117,7 @@ export function mapToDisplay(
progressTotal,
approvalMode: call.approvalMode,
originalRequestName: call.request.originalRequestName,
errorType,
};
});
+2
View File
@@ -16,6 +16,7 @@ import {
type AgentDefinition,
type ApprovalMode,
type Kind,
type ToolErrorType,
CoreToolCallStatus,
checkExhaustive,
} from '@google/gemini-cli-core';
@@ -117,6 +118,7 @@ export interface IndividualToolCallDisplay {
originalRequestName?: string;
progress?: number;
progressTotal?: number;
errorType?: ToolErrorType;
}
export interface CompressionProps {
@@ -11,6 +11,7 @@ import { PolicyDecision } from '../policy/types.js';
import { MessageBusType, type Message } from './types.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { debugLogger } from '../utils/debugLogger.js';
import { coreEvents } from '../utils/events.js';
export class MessageBus extends EventEmitter {
constructor(
@@ -70,6 +71,10 @@ export class MessageBus extends EventEmitter {
break;
case PolicyDecision.DENY:
// Emit both rejection and response messages
coreEvents.emitFeedback(
'error',
`Tool call "${message.toolCall.name}" was blocked by policy.`,
);
this.emitMessage({
type: MessageBusType.TOOL_POLICY_REJECTION,
toolCall: message.toolCall,
+1 -32
View File
@@ -35,11 +35,7 @@ import { runInDevTraceSpan } from '../telemetry/trace.js';
import { logToolCall } from '../telemetry/loggers.js';
import { ToolCallEvent } from '../telemetry/types.js';
import type { EditorType } from '../utils/editor.js';
import {
MessageBusType,
type SerializableConfirmationDetails,
type ToolConfirmationRequest,
} from '../confirmation-bus/types.js';
import { type SerializableConfirmationDetails } from '../confirmation-bus/types.js';
import { runWithToolCallContext } from '../utils/toolCallContext.js';
import {
coreEvents,
@@ -91,9 +87,6 @@ const createErrorResponse = (
* Coordinates execution via state updates and event listening.
*/
export class Scheduler {
// Tracks which MessageBus instances have the legacy listener attached to prevent duplicates.
private static subscribedMessageBuses = new WeakSet<MessageBus>();
private readonly state: SchedulerStateManager;
private readonly executor: ToolExecutor;
private readonly modifier: ToolModificationHandler;
@@ -127,8 +120,6 @@ export class Scheduler {
this.executor = new ToolExecutor(this.context);
this.modifier = new ToolModificationHandler();
this.setupMessageBusListener(this.messageBus);
coreEvents.on(CoreEvent.McpProgress, this.handleMcpProgress);
}
@@ -161,28 +152,6 @@ export class Scheduler {
});
};
private setupMessageBusListener(messageBus: MessageBus): void {
if (Scheduler.subscribedMessageBuses.has(messageBus)) {
return;
}
// TODO: Optimize policy checks. Currently, tools check policy via
// MessageBus even though the Scheduler already checked it.
messageBus.subscribe(
MessageBusType.TOOL_CONFIRMATION_REQUEST,
async (request: ToolConfirmationRequest) => {
await messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: request.correlationId,
confirmed: false,
requiresUserConfirmation: true,
});
},
);
Scheduler.subscribedMessageBuses.add(messageBus);
}
/**
* Schedules a batch of tool calls.
* @returns A promise that resolves with the results of the completed batch.