Files
podman/backend/src/action/hermes.ts
T
Ramis d8eaf8d2e2 feat(voice): speak every intervention, not just critical escalations
Voice was gated to severity === 'critical', so most conflict cards were
silent. Now every intervention is spoken: priority is derived from
severity in publishHermesIntervention — critical jumps the queue,
the rest serialize so concurrent alerts don't garble each other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LuV8W8oNYRsDWKoqK8Mkqc
2026-06-28 10:31:35 -07:00

100 lines
3.0 KiB
TypeScript

import type { Room } from '@livekit/rtc-node';
import { Room as LiveKitRoom } from '@livekit/rtc-node';
import { AccessToken } from 'livekit-server-sdk';
import type { Collision, DataMessage, HermesMessage, Intervention } from '@podman/shared';
import { DATA_TOPIC } from '@podman/shared';
import { env } from '../env.js';
import { speak } from '../voice/live.js';
import { notifyCriticalLiveConversations } from '../live-conversation/sessions.js';
const encoder = new TextEncoder();
function teammateText(collision: Collision, intervention: Intervention): string {
return `${collision.engineers.join(', ')}: ${intervention.message}`;
}
export function createHermesMessage(
collision: Collision,
intervention: Intervention,
): HermesMessage {
return {
id: `hermes_${Date.now()}`,
podId: collision.podId,
interventionId: intervention.id,
recipients: collision.engineers,
text: teammateText(collision, intervention),
urgency: collision.severity === 'critical' ? 'urgent' : 'normal',
createdAt: new Date().toISOString(),
};
}
export async function publishHermesMessage(
room: Room,
collision: Collision,
intervention: Intervention,
): Promise<void> {
const data: DataMessage = {
type: 'HERMES_MESSAGE',
message: createHermesMessage(collision, intervention),
};
await room.localParticipant?.publishData(encoder.encode(JSON.stringify(data)), {
reliable: true,
topic: DATA_TOPIC,
});
}
export async function publishHermesIntervention(
room: Room,
collision: Collision,
intervention: Intervention,
voiceLine?: string,
): Promise<void> {
const data: DataMessage = { type: 'COLLISION', collision, intervention };
await room.localParticipant?.publishData(encoder.encode(JSON.stringify(data)), {
reliable: true,
topic: DATA_TOPIC,
});
await publishHermesMessage(room, collision, intervention);
void notifyCriticalLiveConversations(collision, intervention, voiceLine).catch((err) =>
console.warn(`[live-conversation] critical notify failed: ${(err as Error).message}`),
);
if (voiceLine)
await speak(room, voiceLine, {
priority: collision.severity === 'critical' ? 'critical' : 'normal',
});
}
async function hermesToken(roomName: string): Promise<string> {
const at = new AccessToken(env.LIVEKIT_API_KEY, env.LIVEKIT_API_SECRET, {
identity: `podman-hermes-${Date.now()}`,
name: 'PodMan Hermes',
ttl: '10m',
});
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
return at.toJwt();
}
export async function notifyHermesInterventionInRoom(
roomName: string,
collision: Collision,
intervention: Intervention,
voiceLine?: string,
): Promise<void> {
const room = new LiveKitRoom();
try {
await room.connect(env.LIVEKIT_URL, await hermesToken(roomName), {
autoSubscribe: false,
dynacast: false,
});
await publishHermesIntervention(room, collision, intervention, voiceLine);
} finally {
await room.disconnect().catch(() => {});
}
}