diff --git a/src/renderer/src/lib/ipc-mock.ts b/src/renderer/src/lib/ipc-mock.ts index bb6d118e..47d896aa 100644 --- a/src/renderer/src/lib/ipc-mock.ts +++ b/src/renderer/src/lib/ipc-mock.ts @@ -1,3 +1,9 @@ +import { + classifyBrokerEvent, + type AgentReleasedEvent, + type AgentSpawnedEvent, + type RelayInboundEvent +} from '@shared/schemas/broker-events' import type { AiHistEntry, AiHistRecentOptions, @@ -72,7 +78,7 @@ import type { import { getTerminalRuntime } from '@/lib/terminal-runtime-registry' type BrokerEventLike = Record & { - kind?: string + kind: string projectId?: string name?: string from?: string @@ -642,6 +648,10 @@ function addReconciledMessage(event: BrokerEventLike): void { function handleInjectedBrokerEvent(event: BrokerEventLike): void { const projectId = event.projectId || state.activeId || defaultProject.id const normalized: BrokerEventLike = { ...event, projectId } + const classification = classifyBrokerEvent(normalized) + if (classification.status === 'malformed') { + throw new Error(`ipc-mock: malformed broker event (kind=${classification.kind ?? 'none'}): ${classification.reason}`) + } if (normalized.kind === 'agent_spawned' && normalized.name) { upsertAgent({ name: normalized.name, @@ -797,13 +807,14 @@ export const pearMock: PearAPI = { const agent = upsertAgent({ ...input, projectId, runtime: 'mock', current_state: 'idle' }) handleInjectedBrokerEvent({ kind: 'agent_spawned', - projectId, name: agent.name, + runtime: agent.runtime || 'mock', cli: agent.cli, model: agent.model, + projectId, channels: agent.channels, event_id: `${projectId}:agent:${agent.name}` - }) + } satisfies AgentSpawnedEvent) return { name: agent.name, runtime: agent.runtime || 'mock', cli: agent.cli } }, listPersonas: async (): Promise => [], @@ -828,12 +839,12 @@ export const pearMock: PearAPI = { sendMessage: async (projectId: string | undefined, input: BrokerSendMessageInput) => { handleInjectedBrokerEvent({ kind: 'relay_inbound', - projectId, + event_id: `${projectId || 'mock'}:human:${++seq}`, from: input.from || 'human', target: input.to, body: input.text, - event_id: `${projectId || 'mock'}:human:${++seq}` - }) + projectId + } satisfies RelayInboundEvent) }, reconcileMessages: async (input: BrokerReconcileMessagesInput) => clone(state.messages.filter((message) => message.projectId === input.projectId)), @@ -848,7 +859,7 @@ export const pearMock: PearAPI = { subscribeAgentChannel: async () => undefined, unsubscribeAgentChannel: async () => undefined, releaseAgent: async (projectId: string | undefined, name: string) => { - handleInjectedBrokerEvent({ kind: 'agent_released', projectId, name, event_id: `${projectId || 'mock'}:released:${name}` }) + handleInjectedBrokerEvent({ kind: 'agent_released', name, projectId, event_id: `${projectId || 'mock'}:released:${name}` } satisfies AgentReleasedEvent) }, listAgents: async (projectId?: string) => clone(projectId ? state.agents.filter((agent) => agent.projectId === projectId) : state.agents), @@ -1124,13 +1135,14 @@ export const pearMockHarness: PearMockHarness = { const name = `${prefix}-${String(index + 1).padStart(4, '0')}` events.push({ kind: 'agent_spawned', - projectId, name, + runtime: 'mock', cli: index % 2 === 0 ? 'codex' : 'claude', + projectId, channels: [channel], event_id: `${projectId}:agent_spawned:${name}`, seq: ++seq - }) + } satisfies AgentSpawnedEvent) } pearMockHarness.injectBrokerEvents(events) }, diff --git a/src/shared/schemas/broker-events.ts b/src/shared/schemas/broker-events.ts index 7968c042..2b924673 100644 --- a/src/shared/schemas/broker-events.ts +++ b/src/shared/schemas/broker-events.ts @@ -377,6 +377,40 @@ export const BrokerEventSchema = z.discriminatedUnion('kind', [ export type ValidatedBrokerEvent = z.infer +/** Per-variant inferred types — use with `satisfies` to catch payload drift at compile time. */ +export type AgentSpawnedEvent = Extract +export type AgentReleasedEvent = Extract +export type AgentExitEvent = Extract +export type AgentExitedEvent = Extract +export type AgentContextLowEvent = Extract +export type RelayInboundEvent = Extract +export type WorkerStreamEvent = Extract +export type DeliveryRetryEvent = Extract +export type DeliveryDroppedEvent = Extract +export type DeliveryQueuedEvent = Extract +export type AgentPendingDrainedEvent = Extract +export type AgentInboundDeliveryModeChangedEvent = Extract +export type DeliveryInjectedEvent = Extract +export type DeliveryVerifiedEvent = Extract +export type DeliveryFailedEvent = Extract +export type MessageDeliveryConfirmedEvent = Extract +export type MessageDeliveryFailedEvent = Extract +export type DeliveryActiveEvent = Extract +export type DeliveryAckEvent = Extract +export type ChannelSubscribedEvent = Extract +export type ChannelUnsubscribedEvent = Extract +export type WorkerReadyEvent = Extract +export type WorkerErrorEvent = Extract +export type RelaycastPublishedEvent = Extract +export type RelaycastPublishFailedEvent = Extract +export type AclDeniedEvent = Extract +export type AgentIdleEvent = Extract +export type AgentResultEvent = Extract +export type AgentBlockedOnSendEvent = Extract +export type AgentRestartingEvent = Extract +export type AgentRestartedEvent = Extract +export type AgentPermanentlyDeadEvent = Extract + /** * The shape every forwarded broker event satisfies: an object carrying a * string `kind`. Both validated and (kind-only-validated) unknown events are